3、数据集准备:nuScenes数据集介绍、数据集下载与解压、数据预处理流程、生成BEV标注文件
做BEV感知,数据是第一步。说实话,我见过不少同学模型写得飞起,结果在数据准备上栽了跟头。今天咱们就好好聊聊nuScenes这个数据集,以及怎么把它变成模型能吃的“饲料”。
3.1 nuScenes数据集介绍
nuScenes是目前BEV感知领域最主流的数据集之一。为什么大家都用它?我个人觉得有这几个原因:
- 数据量大:1000个场景,每个场景20秒,总共约40万帧数据
- 传感器齐全:6个相机、1个激光雷达、5个毫米波雷达,还有GPS/IMU
- 标注丰富:23类物体,每帧都有3D框标注,还有属性信息
- 场景多样:波士顿和新加坡两个城市,白天黑夜、晴天雨天都有
我记得第一次接触nuScenes时,光看它的数据结构就花了两天。它的核心概念包括:
| 概念 | 说明 | 我的理解 |
|---|---|---|
| scene | 一个20秒的连续场景 | 相当于一段行车记录 |
| sample | 场景中的关键帧(2Hz) | 每0.5秒取一帧做标注 |
| sample_data | 传感器原始数据 | 相机图片、点云文件等 |
| ego_pose | 自车位姿 | 车在世界坐标系下的位置 |
| calibrated_sensor | 传感器标定参数 | 内参和外参矩阵 |
说白了,nuScenes就是把这些数据组织成了一个关系型数据库。每个sample关联着6张图片、1帧点云,还有对应的标注信息。你想想看,要处理这么多数据,没有一个好的组织方式还真不行。
3.2 数据集下载与解压
下载nuScenes数据集,我建议直接去官网注册。下载前要注意:
下载完成后,你会得到一堆压缩包。解压命令很简单:
# 解压所有tar文件
for f in *.tar; do
tar -xvf "$f"
done
# 或者用并行解压,快很多
ls *.tar | parallel tar -xvf {}
解压后的目录结构应该是这样的:
nuscenes/
├── maps/ # 地图数据
├── samples/ # 关键帧数据
│ ├── CAM_FRONT/
│ ├── CAM_BACK/
│ ├── LIDAR_TOP/
│ └── ...
├── sweeps/ # 非关键帧数据
└── v1.0-trainval/ # 标注文件(JSON格式)
嗯,这里要注意一点:samples和sweeps的区别。samples是标注过的关键帧(2Hz),sweeps是中间帧(20Hz)。做BEV感知时,我们通常只用samples。
3.3 数据预处理流程
拿到原始数据后,不能直接喂给模型。我总结了一套预处理流程,你在项目中可以直接套用:
具体来说,每一步做什么呢?
- 时间同步:不同传感器的采样频率不同,需要对齐到同一时刻
- 坐标系对齐:把激光雷达、相机、毫米波雷达的数据统一到自车坐标系
- 数据增强:翻转、旋转、缩放等,增加数据多样性
- 生成BEV标注:把3D框投影到BEV视角,生成栅格化的标注
我曾经在一个项目中,因为坐标系没对齐,模型训练了三天才发现结果全偏了。后来我养成了一个习惯:每次预处理完,先可视化几帧看看对不对。
下面是一个简单的预处理代码框架:
import numpy as np
from nuscenes import NuScenes
def preprocess_sample(nusc, sample_token):
# 获取sample数据
sample = nusc.get('sample', sample_token)
# 获取激光雷达数据
lidar_data = nusc.get('sample_data', sample['data']['LIDAR_TOP'])
lidar_path = nusc.get_sample_data_path(lidar_data['token'])
# 获取标注
ann_tokens = sample['anns']
# 坐标系转换
# 从传感器坐标系 → 自车坐标系 → 全局坐标系
# 这里省略具体实现...
return lidar_points, annotations
3.4 生成BEV标注文件
这是最关键的一步。BEV标注说白了,就是把3D空间中的物体投影到俯视图上。我常用的方法是:
生成BEV标注的步骤:
- 定义BEV范围(比如前后50m,左右25m)
- 创建栅格地图(每个格子代表一个位置)
- 遍历所有标注框,计算它们在BEV下的投影
- 把投影结果填入栅格(分类、回归两个分支)
代码实现大概是这样的:
def generate_bev_annotation(annotations, bev_range, grid_size):
"""
生成BEV标注
bev_range: [x_min, x_max, y_min, y_max]
grid_size: [x_grids, y_grids]
"""
# 初始化BEV栅格
bev_map = np.zeros((grid_size[0], grid_size[1], 3)) # 3个通道:类别、中心x、中心y
for ann in annotations:
# 获取3D框参数
x, y, z = ann['translation']
w, l, h = ann['size']
yaw = ann['rotation'][3] # 简化处理
# 判断是否在BEV范围内
if not (bev_range[0] <= x <= bev_range[1] and
bev_range[2] <= y <= bev_range[3]):
continue
# 计算在栅格中的位置
grid_x = int((x - bev_range[0]) / (bev_range[1] - bev_range[0]) * grid_size[0])
grid_y = int((y - bev_range[2]) / (bev_range[3] - bev_range[2]) * grid_size[1])
# 填入标注
bev_map[grid_x, grid_y, 0] = ann['category_id'] # 类别
bev_map[grid_x, grid_y, 1] = x # 中心x
bev_map[grid_x, grid_y, 2] = y # 中心y
return bev_map
生成好的BEV标注文件,我一般保存为npy格式。每个sample对应一个npy文件,文件名和sample_token一致。这样训练时直接加载,省去了重复计算的麻烦。
最后,给大家看一张我画的流程图,把整个数据准备过程串起来:
这张图把整个流程串起来了。你从左边开始,一步步往下走,最后就能拿到模型训练需要的数据。我个人建议,第一次做的时候每一步都可视化一下,确保数据没跑偏。
好了,数据准备这块就聊到这儿。记住一句话:数据准备花的时间,会在模型训练阶段加倍还给你。别图快,稳扎稳打才是正道。