传感器配置:相机、激光雷达、GNSS、IMU 的配置与数据获取

说实话,传感器配置这块,是仿真里最「实在」的环节。你算法写得再花哨,传感器数据不对,全是白搭。我在项目里见过太多人,花了一周调模型,最后发现是相机内参设错了——那种感觉,嗯,挺崩溃的。

今天咱们就把 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 里每个传感器都有自己的回调函数,它们可能在不同时间触发。我常用的做法是:

  1. 给每个传感器打上时间戳(data.timestamp)
  2. 用一个队列收集所有数据
  3. 在固定时间窗口内做对齐
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))

下面这张图展示了传感器数据流的整体结构:

传感器数据流与同步架构 RGB 相机 深度相机 语义分割 LiDAR 数据队列 时间戳对齐 帧同步 数据缓存 数据处理 图像解码 点云解析 坐标转换 融合输出

你看,整个流程就是:传感器采集 → 数据入队 → 时间同步 → 处理输出。这个架构我在多个项目里都用过,稳定可靠。

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 跑通,再慢慢加其他传感器。一次配太多,出了问题都不知道是哪个环节的锅。

好了,这一章的内容就到这。传感器配置是仿真实验的「地基」,地基打牢了,后面的自动化批量实验才能跑得稳。

可可精品资料库