4、相机传感器实战:RGB相机配置与图像获取,深度相机与深度图解析,语义分割相机与标签映射,相机内参矩阵与外参

相机传感器,是自动驾驶感知系统里最像「人眼」的部件。但说实话,它比人眼要复杂得多。我刚开始接触CARLA时,以为配个相机就是调个分辨率、设个位置就完事了。结果第一次跑出来的图像全是黑的,或者深度图花得像抽象画……嗯,今天咱们就把这四种相机模式彻底捋一遍。

核心知识点速览

  • RGB相机:最基础的视觉输入,配置参数多,坑也多
  • 深度相机:输出深度图,理解像素到真实距离的映射
  • 语义分割相机:输出标签图,每个像素告诉你「这是什么」
  • 内参矩阵与外参:相机标定的数学本质,决定了图像怎么来的
相机传感器实战 RGB相机 深度相机 语义分割相机 内参矩阵 & 外参 四种相机模式 + 标定参数 = 完整的视觉感知方案 图像 距离 标签

4.1 RGB相机配置与图像获取

RGB相机,说白了就是模拟普通摄像头。它输出的是三通道彩色图像,每个像素由R、G、B三个值组成。在CARLA里配置RGB相机,其实就两步:设置蓝图属性,然后绑定到车辆上。

我个人习惯先把相机蓝图拉出来看看有哪些参数可调。你可以在CARLA的蓝图库里找到 sensor.camera.rgb,它暴露了十几个属性。但真正关键的,其实就这几个:

参数名 说明 我的推荐值
image_size_x 图像宽度(像素) 1280
image_size_y 图像高度(像素) 720
fov 水平视场角(度) 90
sensor_tick 采集间隔(秒) 0.05(即20FPS)

小技巧:FOV不是越大越好。我做过一个测试,FOV设到120度以上,图像边缘畸变会非常明显,对后续的目标检测模型影响很大。一般90度是个比较平衡的值。

配置代码其实很简洁。我一般这样写:

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

# 设置相机位置(前挡风玻璃视角)
transform = carla.Transform(
    carla.Location(x=1.5, z=1.8),
    carla.Rotation(pitch=-5)
)

# 生成相机并绑定到车辆
camera = world.spawn_actor(blueprint, transform, attach_to=vehicle)

# 监听图像数据
camera.listen(lambda image: image.save_to_disk('output/%.6d.png' % image.frame))

这里有个坑——相机位置。我曾经把z设成0.5,结果拍出来的全是地面,根本看不到前方路况。前挡风玻璃视角一般z在1.6~2.0米之间,pitch稍微往下倾斜5度左右,这样既能看远又能看到近处的车道线。

4.2 深度相机与深度图解析

深度相机输出的是深度图。每个像素的值代表该点到相机的距离。但这里有个容易搞混的地方:深度值到底是以什么为单位?

CARLA的深度相机默认输出的是归一化深度,范围在0~1之间。你需要把它映射回真实距离。映射公式很简单:

# 深度图转真实距离(米)
def depth_to_meters(depth_image):
    # depth_image是归一化的0~1值
    # 实际距离 = 1000 * depth_image(单位:米)
    # 但要注意,CARLA的深度编码方式有点特殊
    return depth_image * 1000.0

等等,为什么是乘以1000?我记得第一次用深度相机时,直接拿归一化值当距离用,结果算出来的障碍物距离全是0.5米、0.8米这种离谱值。后来查文档才发现,CARLA的深度编码用了三个通道来存储高精度深度信息。说白了,它把深度值拆成了RGB三个通道来存,每个通道存一部分。

正确的解码方式是这样的:

import numpy as np

def decode_depth(raw_image):
    """
    将CARLA深度相机的原始数据解码为真实距离(米)
    raw_image: 从camera.listen回调中拿到的image数据
    """
    # 转为numpy数组
    img = np.frombuffer(raw_image.raw_data, dtype=np.uint8)
    img = img.reshape((raw_image.height, raw_image.width, 4))
    
    # 提取BGR通道(注意CARLA是BGRA格式)
    b = img[:,:,0].astype(np.float32)
    g = img[:,:,1].astype(np.float32)
    r = img[:,:,2].astype(np.float32)
    
    # 解码:深度 = (R + G*256 + B*256*256) / (256*256*256 - 1)
    depth = (r + g * 256.0 + b * 256.0 * 256.0)
    depth = depth / (256.0 * 256.0 * 256.0 - 1.0)
    
    # 乘以最大距离(默认1000米)
    depth = depth * 1000.0
    return depth

注意:深度相机的有效范围是有限的。CARLA默认最大深度是1000米,但实际场景中超过100米的深度值噪声就很大了。我一般会做个截断,只保留0~80米范围内的深度值,再往上就当无效数据处理。

深度图有什么用?说白了,它就是给RGB图像配上了「距离信息」。你可以用它来做障碍物测距、可行驶区域判断,甚至重建3D点云。我做过一个项目,用深度图配合RGB图像做车道线检测,在弯道场景的准确率提升了将近15%。

4.3 语义分割相机与标签映射

语义分割相机,是CARLA里我最喜欢的一个传感器。它输出的图像里,每个像素的值代表一个语义类别。比如,路面是0,车道线是1,车辆是10,行人是4……

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

blueprint = world.get_blueprint_library().find('sensor.camera.semantic_segmentation')

但这里有个关键点——标签映射。CARLA的语义分割相机输出的是整数标签,你需要把这些整数映射成可读的类别名称,或者映射成颜色才能可视化。

CARLA官方定义了一套标签体系,总共24个类别。我常用的几个:

标签值 类别 推荐颜色(RGB)
0 未标注 (0, 0, 0)
1 路面 (128, 128, 128)
2 车道线 (255, 255, 255)
4 行人 (220, 20, 60)
10 车辆 (0, 0, 142)
12 交通标志 (220, 220, 0)

我习惯把标签图转成彩色可视化图,方便调试时一眼看出场景里都有什么:

# 标签转彩色图
def labels_to_cityscapes(label_img, carla_labels):
    """
    label_img: 语义分割相机输出的单通道标签图
    carla_labels: 标签到颜色的映射字典
    """
    h, w = label_img.shape
    color_img = np.zeros((h, w, 3), dtype=np.uint8)
    
    for label_id, color in carla_labels.items():
        mask = (label_img == label_id)
        color_img[mask] = color
    
    return color_img

避坑指南:我曾经在训练语义分割模型时,直接用CARLA的标签图当ground truth。结果发现模型在真实场景上表现很差。后来才意识到,CARLA的标签定义和真实数据集(比如Cityscapes)不完全一致。比如CARLA把「自行车」和「摩托车」归为一类,但Cityscapes是分开的。所以做迁移学习时,一定要先做标签对齐。

4.4 相机内参矩阵与外参

这部分是相机传感器的「数学灵魂」。内参矩阵描述的是相机本身的属性,外参描述的是相机在世界坐标系中的位置和朝向。

内参矩阵长这样:

     [fx  0  cx]
K =  [ 0  fy cy]
     [ 0   0  1]

其中fx、fy是焦距(以像素为单位),cx、cy是主点坐标(通常是图像中心)。在CARLA里,你可以通过相机的属性拿到这些值:

# 获取相机内参
camera = world.get_actors().filter('sensor.camera.rgb')[0]
intrinsic = camera.get_intrinsic_matrix()
print(intrinsic)
# 输出类似:
# [[fx,  0, cx],
#  [ 0, fy, cy],
#  [ 0,  0,  1]]

fx和fy怎么算出来的?其实和图像分辨率、FOV有关。公式是:

fx = image_size_x / (2 * tan(fov/2))
fy = image_size_y / (2 * tan(fov/2))

举个例子,1280x720分辨率、FOV=90度:

import math
fx = 1280 / (2 * math.tan(math.radians(90/2)))  # 约640
fy = 720 / (2 * math.tan(math.radians(90/2)))   # 约360

外参描述的是相机在世界坐标系中的位置和朝向。在CARLA里,外参其实就是你创建相机时传入的Transform:

transform = carla.Transform(
    carla.Location(x=1.5, z=1.8),
    carla.Rotation(pitch=-5, yaw=0, roll=0)
)

这个Transform可以转成4x4的外参矩阵:

# 获取相机外参
extrinsic = camera.get_transform().get_matrix()
print(extrinsic)
# 4x4矩阵,包含旋转和平移

为什么内参外参这么重要?

因为你要做「像素到世界坐标」的映射。比如你从RGB图像里检测到一个障碍物,它的像素坐标是(u, v),你想知道它在真实世界中的位置。这时候就需要内参和外参配合:

# 像素坐标 -> 相机坐标系 -> 世界坐标系
# 1. 像素转相机坐标(需要内参)
# 2. 相机坐标转世界坐标(需要外参的逆矩阵)

我做过一个多相机融合的项目,四个相机分别装在车的前后左右。如果没有精确的内参外参,拼接出来的全景图像全是错位的。调参调了整整两天……

最后说一句,内参外参的标定精度直接影响后续所有感知算法的效果。如果你只是做简单的仿真测试,用CARLA默认的参数就够了。但如果你要做高精度的传感器融合或者3D重建,建议自己写个标定脚本,用棋盘格在CARLA里跑一遍标定流程。嗯,这个我们后面章节会详细讲。

个人经验:每次换相机位置或分辨率后,记得重新获取内参和外参。我吃过一次亏——换了相机位置后忘了更新外参,结果融合出来的点云全部偏移了2米,排查了半天才发现是外参没更新。

好了,四种相机模式加上内参外参,构成了CARLA视觉感知的完整基础。下一章我们会把这些传感器组合起来,做一个真正的多传感器融合系统。

可可精品资料库