3、Carla传感器详解:RGB相机、深度相机、语义分割相机、Lidar、Radar的配置与数据读取

各位同学,欢迎来到第三章。前面我们把Carla的环境搭好了,也跑通了第一个Hello World脚本。今天咱们来点硬核的——传感器。

说实话,传感器是自动驾驶的「眼睛」。没有它们,算法再牛也是白搭。我在做感知部署的时候,踩过最多的坑就是传感器配置不对,数据读出来全是乱的。今天咱们就把Carla里最常用的五种传感器讲透:RGB相机、深度相机、语义分割相机、Lidar和Radar。

3.1 传感器基础:蓝图与配置

在Carla里,所有传感器都是通过蓝图(Blueprint)生成的。你可以把蓝图理解成「传感器模板」,里面定义了传感器的型号、参数、挂载位置等。

我个人习惯先把传感器蓝图库拉出来看看,心里有个数:

# 获取所有传感器蓝图
blueprint_library = world.get_blueprint_library()
sensor_blueprints = blueprint_library.filter('sensor.*')

for bp in sensor_blueprints:
    print(bp.id)

运行后你会看到类似这样的输出:

sensor.camera.rgb
sensor.camera.depth
sensor.camera.semantic_segmentation
sensor.lidar.ray_cast
sensor.other.radar

嗯,这就是我们今天要搞定的五个家伙。

小提示: 每个传感器蓝图都有可配置的属性,比如图像分辨率、视场角(FOV)、激光雷达的线束数等。建议用 bp.get_attributes() 查看所有可调参数。

3.2 RGB相机:最基础的视觉传感器

RGB相机就是咱们平时用的彩色摄像头。在感知任务里,它负责提供最直观的视觉信息——车道线、交通灯、行人、车辆,全靠它。

配置RGB相机其实很简单,但有几个参数我建议你重点关注:

参数 说明 推荐值
image_size_x 图像宽度 1280
image_size_y 图像高度 720
fov 视场角(度) 90
sensor_tick 采集频率(秒) 0.05(即20FPS)

来看一段完整的配置代码:

# 创建RGB相机蓝图
rgb_bp = blueprint_library.find('sensor.camera.rgb')
rgb_bp.set_attribute('image_size_x', '1280')
rgb_bp.set_attribute('image_size_y', '720')
rgb_bp.set_attribute('fov', '90')
rgb_bp.set_attribute('sensor_tick', '0.05')

# 挂载到车辆前挡风玻璃位置
transform = carla.Transform(carla.Location(x=1.5, z=2.4))
rgb_camera = world.spawn_actor(rgb_bp, transform, attach_to=vehicle)

# 定义回调函数,处理图像数据
def process_rgb_image(image):
    # 将原始数据转为numpy数组
    array = np.frombuffer(image.raw_data, dtype=np.uint8)
    array = array.reshape((image.height, image.width, 4))
    # 去掉alpha通道,保留RGB
    rgb_array = array[:, :, :3]
    # 这里就可以做后续处理了,比如保存或送入模型
    cv2.imshow('RGB Camera', rgb_array)
    cv2.waitKey(1)

# 启动传感器监听
rgb_camera.listen(lambda image: process_rgb_image(image))

注意看那个 image.raw_data,它返回的是BGRA格式的字节流。我刚开始做的时候直接当RGB用,结果颜色全乱了。后来才发现需要先转成numpy数组,再取前三个通道。

避坑指南: 我曾经在项目里直接用了 image.raw_data 的原始字节,没做reshape,结果图像是歪的。记住:一定要用 np.frombuffer 转成数组,再 reshape 成 (height, width, 4) 的形状。

3.3 深度相机:给图像加上距离信息

深度相机和RGB相机长得一模一样,但它的每个像素存储的不是颜色,而是距离值。说白了,就是告诉你「这个物体离我有多远」。

配置方式和RGB几乎一样,只是蓝图名字不同:

depth_bp = blueprint_library.find('sensor.camera.depth')
depth_bp.set_attribute('image_size_x', '1280')
depth_bp.set_attribute('image_size_y', '720')
depth_bp.set_attribute('fov', '90')
depth_bp.set_attribute('sensor_tick', '0.05')

depth_camera = world.spawn_actor(depth_bp, transform, attach_to=vehicle)

关键区别在于数据读取。深度图像的数据需要做归一化处理,才能得到真实的距离值(单位:米)。

def process_depth_image(image):
    # 原始数据是BGRA格式,但深度信息编码在R、G、B通道中
    array = np.frombuffer(image.raw_data, dtype=np.uint8)
    array = array.reshape((image.height, image.width, 4))
    
    # 提取深度信息:Carla用RGB编码深度
    # 公式:depth = R + G * 256 + B * 256 * 256
    depth_in_meters = array[:, :, 2] * 1.0 + \
                      array[:, :, 1] * 256.0 + \
                      array[:, :, 0] * 256.0 * 256.0
    depth_in_meters = depth_in_meters / (256.0 * 256.0 * 256.0 - 1.0)
    depth_in_meters = depth_in_meters * 1000.0  # 转为米
    
    # 显示深度图(归一化到0-255)
    depth_display = np.clip(depth_in_meters / 100.0, 0, 1) * 255
    depth_display = depth_display.astype(np.uint8)
    cv2.imshow('Depth Camera', depth_display)
    cv2.waitKey(1)

你想想看,为什么Carla要用RGB三个通道来编码深度?其实是为了节省带宽。一个32位的浮点数需要4个字节,而用RGB三个8位整数就能表示0到16777215的范围,精度也够用。

重点: 深度相机的数据范围是0到1000米。超过1000米的物体,深度值会被截断。在实际部署中,我一般只关注0到50米范围内的物体,再远的意义不大。

3.4 语义分割相机:让每个像素都有「身份」

语义分割相机是我个人最喜欢的一个传感器。它输出的图像里,每个像素都对应一个语义标签——比如行人、车辆、道路、建筑等。说白了,它帮你把图像里的东西「分类」好了。

配置代码和前面几乎一样:

seg_bp = blueprint_library.find('sensor.camera.semantic_segmentation')
seg_bp.set_attribute('image_size_x', '1280')
seg_bp.set_attribute('image_size_y', '720')
seg_bp.set_attribute('fov', '90')
seg_bp.set_attribute('sensor_tick', '0.05')

seg_camera = world.spawn_actor(seg_bp, transform, attach_to=vehicle)

数据读取时,每个像素的值是一个整数,代表物体类别。Carla预定义了多种类别,比如:

标签值 类别 颜色(R,G,B)
0 未标注 (0, 0, 0)
1 建筑 (70, 70, 70)
4 行人 (220, 20, 60)
10 车辆 (0, 0, 142)
12 道路 (128, 64, 128)

读取并可视化语义分割图像:

def process_seg_image(image):
    array = np.frombuffer(image.raw_data, dtype=np.uint8)
    array = array.reshape((image.height, image.width, 4))
    
    # 语义标签在R通道中
    seg_labels = array[:, :, 2]
    
    # 为了可视化,将标签映射为彩色
    # Carla提供了内置的颜色映射函数
    image.convert(carla.ColorConverter.CityScapesPalette)
    seg_array = np.frombuffer(image.raw_data, dtype=np.uint8)
    seg_array = seg_array.reshape((image.height, image.width, 4))
    seg_rgb = seg_array[:, :, :3]
    
    cv2.imshow('Semantic Segmentation', seg_rgb)
    cv2.waitKey(1)

注意那个 convert(carla.ColorConverter.CityScapesPalette),它会自动把标签值映射成Cityscapes数据集的配色方案。我在做模型训练时,经常用这个来生成伪标签,省了不少标注的功夫。

小技巧: 如果你需要自定义颜色映射,可以自己写一个字典,把标签值映射成你想要的RGB颜色。比如我习惯把行人标成红色,车辆标成蓝色,这样一眼就能看出来。

3.5 Lidar:自动驾驶的「3D眼睛」

Lidar(激光雷达)是自动驾驶里最重要的传感器之一。它通过发射激光束,测量周围物体的距离和形状,生成3D点云数据。

Carla里默认的Lidar是64线束的,但你可以根据需要调整:

lidar_bp = blueprint_library.find('sensor.lidar.ray_cast')
lidar_bp.set_attribute('channels', '64')          # 线束数
lidar_bp.set_attribute('range', '100')            # 探测距离(米)
lidar_bp.set_attribute('points_per_second', '1000000')  # 每秒点数
lidar_bp.set_attribute('rotation_frequency', '20')      # 旋转频率(Hz)
lidar_bp.set_attribute('upper_fov', '10')         # 上视场角(度)
lidar_bp.set_attribute('lower_fov', '-30')        # 下视场角(度)

# 挂载到车顶
lidar_transform = carla.Transform(carla.Location(x=0.0, z=2.5))
lidar = world.spawn_actor(lidar_bp, lidar_transform, attach_to=vehicle)

Lidar的数据读取和相机不太一样,它返回的是点云数据:

def process_lidar_data(point_cloud):
    # 点云数据包含:x, y, z, intensity
    # 每个点4个float32值
    points = np.frombuffer(point_cloud.raw_data, dtype=np.float32)
    points = points.reshape((-1, 4))
    
    # 提取坐标和强度
    x = points[:, 0]
    y = points[:, 1]
    z = points[:, 2]
    intensity = points[:, 3]
    
    # 可视化(简单投影到2D)
    # 这里只做演示,实际会用Open3D或Matplotlib
    print(f"接收到 {len(points)} 个点")
    print(f"X范围: {x.min():.2f} ~ {x.max():.2f}")
    print(f"Y范围: {y.min():.2f} ~ {y.max():.2f}")
    print(f"Z范围: {z.min():.2f} ~ {z.max():.2f}")

lidar.listen(lambda data: process_lidar_data(data))

为什么Lidar的数据是float32?因为每个点需要存储三维坐标和反射强度,用浮点数才能保证精度。我在做点云检测时,经常需要过滤掉地面点(z值接近0的点),不然算力都浪费在地面上了。

注意: Lidar的点数不是固定的,它取决于 points_per_secondrotation_frequency。比如每秒100万个点,旋转频率20Hz,那么每帧就是5万个点。如果场景里物体多,点数会略有波动。

3.6 Radar:毫米波雷达的「速度感知」

Radar(毫米波雷达)和Lidar不同,它不仅能测距离,还能测速度。说白了,它能告诉你「那个物体正在以多快的速度靠近你」。

配置Radar:

radar_bp = blueprint_library.find('sensor.other.radar')
radar_bp.set_attribute('horizontal_fov', '30')     # 水平视场角(度)
radar_bp.set_attribute('vertical_fov', '10')       # 垂直视场角(度)
radar_bp.set_attribute('range', '100')             # 探测距离(米)
radar_bp.set_attribute('points_per_second', '1500') # 每秒探测点数

radar_transform = carla.Transform(carla.Location(x=1.5, z=1.0))
radar = world.spawn_actor(radar_bp, radar_transform, attach_to=vehicle)

Radar的数据结构比较特殊,每个探测点包含:距离、方位角、仰角、速度:

def process_radar_data(radar_data):
    # 每个点包含:距离、方位角、仰角、速度、信噪比
    # 数据格式:[[azimuth, altitude, depth, velocity, intensity], ...]
    points = np.frombuffer(radar_data.raw_data, dtype=np.float32)
    points = points.reshape((-1, 5))
    
    for point in points:
        azimuth = point[0]    # 方位角(弧度)
        altitude = point[1]   # 仰角(弧度)
        depth = point[2]      # 距离(米)
        velocity = point[3]   # 速度(米/秒,正数表示靠近)
        intensity = point[4]  # 信噪比
        
        # 只关注靠近的物体(速度为正)
        if velocity > 0 and depth < 50:
            print(f"前方 {depth:.1f} 米处有物体,速度 {velocity:.1f} m/s")

radar.listen(lambda data: process_radar_data(data))

Radar的velocity字段很有意思。正值表示物体正在靠近你,负值表示远离。我在做前向碰撞预警时,就靠这个来判断是否需要刹车。

经验之谈: Radar的点数比Lidar少得多,但它的速度信息是Lidar没有的。在实际项目中,我通常把Lidar和Radar融合使用——Lidar负责精确的3D定位,Radar负责速度估计。两者互补,效果很好。

3.7 多传感器同步采集

在实际部署中,你不可能只用一个传感器。多个传感器同时工作,数据怎么同步?

我的做法是用一个全局回调函数,把所有传感器的数据收集起来,统一处理:

class SensorDataCollector:
    def __init__(self):
        self.rgb_image = None
        self.depth_image = None
        self.seg_image = None
        self.lidar_data = None
        self.radar_data = None
        self.frame_count = 0
    
    def collect_rgb(self, image):
        self.rgb_image = image
        self.frame_count += 1
    
    def collect_depth(self, image):
        self.depth_image = image
    
    def collect_seg(self, image):
        self.seg_image = image
    
    def collect_lidar(self, data):
        self.lidar_data = data
    
    def collect_radar(self, data):
        self.radar_data = data
    
    def process_all(self):
        if self.rgb_image is not None:
            # 处理所有传感器数据
            print(f"第 {self.frame_count} 帧数据已收集")
            # 这里可以调用你的感知算法

collector = SensorDataCollector()

# 启动所有传感器
rgb_camera.listen(lambda image: collector.collect_rgb(image))
depth_camera.listen(lambda image: collector.collect_depth(image))
seg_camera.listen(lambda image: collector.collect_seg(image))
lidar.listen(lambda data: collector.collect_lidar(data))
radar.listen(lambda data: collector.collect_radar(data))

# 主循环
while True:
    collector.process_all()
    time.sleep(0.05)  # 20FPS

你可能会问:这些传感器的时间戳能对齐吗?Carla内部会为每个传感器数据打上时间戳,但不同传感器的采集时刻可能略有差异。如果你需要严格同步,可以用Carla的 world.on_tick() 回调来触发采集。

3.8 本章小结

好了,今天的内容就到这里。我们讲了五种传感器的配置和数据读取:

  • RGB相机:彩色图像,注意BGRA转RGB
  • 深度相机:距离信息,注意RGB编码解码
  • 语义分割相机:像素级标签,用Cityscapes配色可视化
  • Lidar:3D点云,注意过滤地面点
  • Radar:距离+速度,注意速度的正负含义

下一章,我们会把这些传感器数据喂给深度学习模型,做目标检测和语义分割。到时候你会发现,今天学的这些配置和读取方法,是后面所有算法的基础。

嗯,今天就到这里。有问题随时问我,咱们下章见。