3. 数据集准备

做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数据集,我建议直接去官网注册。下载前要注意:

注意:完整数据集大约400GB!如果你硬盘不够,可以先下载mini版(约5GB)做实验。

下载完成后,你会得到一堆压缩包。解压命令很简单:

# 解压所有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标注

具体来说,每一步做什么呢?

  1. 时间同步:不同传感器的采样频率不同,需要对齐到同一时刻
  2. 坐标系对齐:把激光雷达、相机、毫米波雷达的数据统一到自车坐标系
  3. 数据增强:翻转、旋转、缩放等,增加数据多样性
  4. 生成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栅格的分辨率一般设为0.1m或0.2m。分辨率太高内存扛不住,太低精度不够。我一般用0.15m,效果不错。

生成BEV标注的步骤:

  1. 定义BEV范围(比如前后50m,左右25m)
  2. 创建栅格地图(每个格子代表一个位置)
  3. 遍历所有标注框,计算它们在BEV下的投影
  4. 把投影结果填入栅格(分类、回归两个分支)

代码实现大概是这样的:

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一致。这样训练时直接加载,省去了重复计算的麻烦。

避坑指南:我曾经犯过一个错误——把BEV标注的范围设得太小,导致远处的物体全被截断了。后来我改成前后50m、左右25m,基本覆盖了所有有效目标。另外,别忘了处理遮挡关系,后面的物体被前面的挡住了,标注时要按最近物体为准。

最后,给大家看一张我画的流程图,把整个数据准备过程串起来:

nuScenes数据预处理流程 原始数据 时间同步 坐标系对齐 数据增强 生成BEV标注 训练数据 传感器原始数据 对齐到同一时刻 统一到自车坐标系 翻转/旋转/缩放 3D框→BEV栅格投影 npy文件

这张图把整个流程串起来了。你从左边开始,一步步往下走,最后就能拿到模型训练需要的数据。我个人建议,第一次做的时候每一步都可视化一下,确保数据没跑偏。

好了,数据准备这块就聊到这儿。记住一句话:数据准备花的时间,会在模型训练阶段加倍还给你。别图快,稳扎稳打才是正道。