4、数据预处理:nuScenes数据格式解析、BEV视角下的坐标变换、内外参标定、数据增强策略
各位同学,欢迎来到第四章。说实话,做BEV感知,数据预处理这块要是没搞明白,后面模型训练基本就是瞎忙活。我自己刚入行那会儿,就被nuScenes的数据格式折腾得够呛,坐标变换更是绕得我头晕。今天咱们就把这些硬骨头一块块啃下来。
4.1 nuScenes数据格式解析
nuScenes数据集,目前自动驾驶领域最常用的benchmark之一。它包含了1000个场景,每个场景20秒,关键帧2Hz。嗯,这里要注意,它用的是32线激光雷达,但标注却是基于整个20秒的点云融合,所以标注质量相当高。
先看看它的核心数据结构:
# nuScenes数据加载示例
from nuscenes.nuscenes import NuScenes
nusc = NuScenes(version='v1.0-trainval', dataroot='/data/nuscenes', verbose=True)
# 查看一个样本的结构
sample = nusc.sample[0]
print(sample.keys())
# dict_keys(['token', 'timestamp', 'prev', 'next', 'scene_token',
# 'data', 'anns'])
我个人习惯把nuScenes的数据分成三层理解:
- Scene层:一个场景,包含多个sample。每个场景有唯一的scene_token。
- Sample层:一个关键帧,包含所有传感器的数据。关键字段是data字典,里面存了各个传感器的token。
- Annotation层:标注信息,包括3D框、类别、属性等。每个sample对应多个anns。
这里有个坑,我刚开始做的时候踩过——nuScenes的坐标系是右前上的(RFLU),而很多BEV模型默认用的是前左上的(FLU)。坐标轴顺序不一样,直接导致我第一版模型训练出来,检测框全歪了。所以拿到数据第一步,先确认坐标系定义。
核心字段速查表
| 字段 | 含义 | 类型 |
|---|---|---|
| token | 唯一标识符 | str |
| timestamp | 时间戳(微秒) | int |
| data | 传感器数据索引 | dict |
| anns | 标注列表 | list[str] |
| ego_pose | 自车位姿 | dict |
| calibrated_sensor | 传感器标定参数 | dict |
4.2 BEV视角下的坐标变换
坐标变换,说白了就是把不同传感器看到的东西,统一到同一个坐标系下。BEV视角下,我们通常需要做三件事:
- 传感器坐标系 → 自车坐标系:用外参矩阵做刚体变换
- 自车坐标系 → 全局坐标系:用自车位姿做变换
- 全局坐标系 → BEV网格:投影到俯视图的像素网格上
我建议你记住这个公式:
# 坐标变换核心代码
def transform_point(point, from_sensor, to_sensor, nusc):
"""
将点从from_sensor坐标系变换到to_sensor坐标系
"""
# 获取传感器标定参数
calib_from = nusc.get('calibrated_sensor', from_sensor['calibrated_sensor_token'])
calib_to = nusc.get('calibrated_sensor', to_sensor['calibrated_sensor_token'])
# 构建变换矩阵
T_from = np.eye(4)
T_from[:3, :3] = Quaternion(calib_from['rotation']).rotation_matrix
T_from[:3, 3] = calib_from['translation']
T_to = np.eye(4)
T_to[:3, :3] = Quaternion(calib_to['rotation']).rotation_matrix
T_to[:3, 3] = calib_to['translation']
# 变换:先转到全局,再转到目标传感器
point_homo = np.append(point, 1)
point_global = T_from @ point_homo
point_target = np.linalg.inv(T_to) @ point_global
return point_target[:3]
为什么会这么复杂?因为每个传感器安装位置不同,朝向也不同。摄像头装在车顶,激光雷达装在保险杠,它们的坐标系原点差着好几米呢。你想想看,如果不做变换,直接把摄像头看到的障碍物位置映射到BEV上,那误差能大到离谱。
我的经验:做坐标变换时,建议先用nuScenes自带的visualization工具验证一下。把点云投影到图像上,看看对齐效果。如果投影后边缘对不上,八成是变换矩阵搞反了。
4.3 内外参标定
内外参标定,这是自动驾驶感知的基石。内参决定了相机本身的成像特性,外参决定了相机在车上的安装位置。
内参矩阵长这样:
K = [[fx, 0, cx],
[ 0, fy, cy],
[ 0, 0, 1]]
fx、fy是焦距,cx、cy是光心偏移。这些参数在nuScenes的calibrated_sensor里可以直接拿到。但我提醒你一句:实际使用中,内参可能会因为温度、震动等因素发生微小变化。我曾经在一个项目中,因为摄像头长时间工作发热,内参漂移了0.3%,导致BEV投影误差累积到半米多。所以有条件的话,建议定期重新标定。
外参包含旋转和平移:
- 旋转:用四元数表示,注意要归一化
- 平移:单位是米,表示传感器相对于自车的位置
这里有个容易搞混的地方——外参的参考系。nuScenes里,calibrated_sensor的translation和rotation是传感器坐标系到自车坐标系的变换。也就是说,如果你想把激光雷达的点变换到自车坐标系,直接用这个参数就行。
避坑指南:我曾经遇到过一个问题——nuScenes的ego_pose和calibrated_sensor的变换顺序搞反了。正确的顺序是:先做calibrated_sensor变换(传感器→自车),再做ego_pose变换(自车→全局)。反过来就会得到完全错误的结果。
4.4 数据增强策略
数据增强,说白了就是让模型见过更多样的情况。BEV感知的数据增强和图像分类不太一样,因为我们要保持3D空间的几何一致性。
常用的增强策略:
4.4.1 翻转(Flip)
水平翻转是最安全的增强方式。为什么?因为道路场景左右对称性很强。但要注意:
- 翻转后,标注框的朝向也要跟着翻转
- 车道线、路沿等语义信息也要翻转
- 速度、加速度等矢量要取反
def flip_bev(points, labels, flip_prob=0.5):
"""水平翻转BEV数据"""
if np.random.random() < flip_prob:
# 翻转x坐标
points[:, 0] = -points[:, 0]
# 翻转朝向角
labels['yaw'] = -labels['yaw']
# 翻转速度
labels['velocity_x'] = -labels['velocity_x']
return points, labels
4.4.2 旋转(Rotation)
旋转增强可以模拟车辆转弯时的视角变化。我个人习惯用±30°的随机旋转。但要注意:
- 旋转中心要选在自车位置,而不是原点
- 旋转后,标注框的角点要重新计算
- BEV网格的边界要重新裁剪
关键点:旋转增强时,一定要保持标注框和点云的相对位置关系不变。我见过有人直接旋转图像,但标注框没跟着转,结果模型学到的全是错误对应关系。
4.4.3 缩放(Scale)
缩放增强可以模拟不同距离下的尺度变化。但BEV的缩放和图像缩放不一样:
- BEV是等距投影,缩放相当于改变分辨率
- 缩放因子建议在[0.8, 1.2]之间
- 缩放后,标注框的尺寸也要等比缩放
def scale_bev(points, labels, scale_range=(0.8, 1.2)):
"""缩放BEV数据"""
scale = np.random.uniform(*scale_range)
# 缩放坐标
points[:, :2] *= scale
# 缩放标注框尺寸
labels['width'] *= scale
labels['length'] *= scale
# 注意:高度不缩放,因为BEV是俯视图
return points, labels
4.5 实战中的注意事项
最后,分享几个我在项目中积累的经验:
- 数据增强的顺序很重要:我建议先做翻转,再做旋转,最后做缩放。因为翻转和旋转都是刚体变换,缩放是非刚体变换,顺序搞反了会导致几何不一致。
- 增强强度要适中:不是越强越好。我曾经把旋转角度设到±90°,结果模型学到的全是旋转后的噪声,反而在直道上表现变差了。
- 验证集不要做增强:这个应该不用我说了吧?但确实有人犯过这个错误。
- 多传感器数据要同步增强:如果你同时用相机和激光雷达,增强操作要对所有传感器一致。不能相机做了翻转,激光雷达没做,那数据就对不上了。
我的建议:刚开始做BEV项目时,先把数据预处理和增强的pipeline写好,用可视化工具反复验证。确保每一步变换都是正确的,再开始训练模型。这一步省下来的时间,远比你后面debug模型要划算得多。
好了,这一章的内容就到这里。数据预处理看似繁琐,但它是整个BEV感知pipeline的基石。下一章我们会讲如何构建BEV特征,把多模态数据融合到一起。到时候你会发现,前面做的这些坐标变换和标定工作,都是为那一步打基础的。