Nuscenes数据集实战:从数据加载到3D目标检测全流程解析

自动驾驶技术的快速发展离不开高质量数据集的支撑。在众多公开数据集中,Nuscenes以其丰富的传感器配置和精细的标注质量脱颖而出,成为3D目标检测领域的重要基准。本文将带您深入探索Nuscenes数据集的核心结构,并通过Python代码实战演示从数据加载到3D检测模型训练的全流程。

1. Nuscenes数据集概览与准备工作

Nuscenes数据集由1000个精心采集的驾驶场景组成,每个场景时长20秒,包含来自6个摄像头、1个激光雷达和5个毫米波雷达的多模态数据。与KITTI等传统数据集相比,Nuscenes最显著的特点是采用了基于token的关联索引系统,这使得数据组织更加灵活,但也增加了初学者的理解难度。

1.1 数据集获取与安装

首先需要从Nuscenes官网下载数据集,推荐从v1.0-mini版本开始入手:

# 安装官方开发工具包
pip install nuscenes-devkit

数据集目录结构通常如下:

v1.0-mini/
├── samples/          # 关键帧传感器数据
├── sweeps/           # 非关键帧传感器数据  
├── maps/             # 高精地图
└── v1.0-mini/        # 元数据JSON文件

1.2 核心数据结构解析

Nuscenes采用七层数据关联结构:

层级 描述 关键字段
Scene 20秒连续驾驶场景 first_sample_token, last_sample_token
Sample 2Hz标注关键帧 data(传感器token), anns(标注token)
SampleData 传感器原始数据 calibrated_sensor_token, ego_pose_token
SampleAnnotation 3D标注框 instance_token, category_name
Instance 物体实例轨迹 first_annotation_token, last_annotation_token
Category 物体类别 name (如vehicle.car)
Attribute 物体状态 name (如vehicle.moving)

2. 数据加载与可视化实战

2.1 初始化数据集对象

from nuscenes import NuScenes

# 初始化数据集接口
nusc = NuScenes(version='v1.0-mini', 
               dataroot='/path/to/v1.0-mini',
               verbose=True)

初始化后会加载所有JSON元数据,典型输出:

====== Loading NuScenes tables for version v1.0-mini...
23 category, 8 attribute, 4 visibility, 911 instance, 
12 sensor, 120 calibrated_sensor, 31206 ego_pose, 
8 log, 10 scene, 404 sample, 31206 sample_data, 
18538 sample_annotation, 4 map
Done loading in 0.356 seconds.

2.2 场景与样本遍历

获取第一个场景及其样本序列:

scene = nusc.scene[0]
print(f"场景描述: {scene['description']}")

# 获取该场景的第一个样本
first_sample = nusc.get('sample', scene['first_sample_token'])

# 遍历场景中的所有样本
current_sample = first_sample
while current_sample['next'] != '':
    print(f"样本时间戳: {current_sample['timestamp']}")
    current_sample = nusc.get('sample', current_sample['next'])

2.3 点云数据加载与可视化

加载激光雷达数据并可视化:

# 获取激光雷达数据
lidar_data = nusc.get('sample_data', first_sample['data']['LIDAR_TOP'])
print(f"点云文件路径: {lidar_data['filename']}")

# 可视化点云和标注
nusc.render_sample_data(lidar_data['token'])

关键点云处理代码:

from pyquaternion import Quaternion

def load_point_cloud(file_path):
    """加载二进制点云数据"""
    points = np.fromfile(file_path, dtype=np.float32)
    return points.reshape(-1, 5)  # x,y,z,intensity,ring_index

def transform_to_ego_frame(points, calibrated_sensor):
    """将点云转换到自车坐标系"""
    # 获取标定参数
    rotation = Quaternion(calibrated_sensor['rotation'])
    translation = np.array(calibrated_sensor['translation'])
    
    # 应用变换
    points[:, :3] = rotation.rotation_matrix @ points[:, :3].T + translation.reshape(3,1)
    return points

3. 3D标注数据处理技巧

3.1 标注框解析与转换

Nuscenes中的标注框使用全局坐标系,需要转换到传感器坐标系:

def box_to_sensor_frame(box, calibrated_sensor):
    """将全局标注框转换到传感器坐标系"""
    # 获取传感器标定
    cs_record = nusc.get('calibrated_sensor', 
                        calibrated_sensor['token'])
    
    # 全局到自车的变换
    global_to_ego = transform_matrix(
        cs_record['translation'],
        Quaternion(cs_record['rotation']),
        inverse=True)
    
    # 应用变换
    box.translate(-np.array(cs_record['translation']))
    box.rotate(Quaternion(cs_record['rotation']).inverse)
    
    return box

3.2 标注统计分析

对数据集进行统计分析有助于理解数据分布:

from collections import defaultdict

category_stats = defaultdict(int)

for ann in nusc.sample_annotation:
    category_stats[ann['category_name']] += 1

# 输出类别分布
print("标注类别统计:")
for cat, count in sorted(category_stats.items(), key=lambda x: -x[1]):
    print(f"{cat:30s}: {count:4d}")

典型输出结果:

vehicle.car                  : 8432
human.pedestrian.adult       : 4215
vehicle.truck                : 1567
human.pedestrian.child       : 892

4. 构建3D目标检测数据管道

4.1 自定义数据加载器

from torch.utils.data import Dataset

class NuScenesDataset(Dataset):
    def __init__(self, nusc, split='train'):
        self.nusc = nusc
        self.samples = self._prepare_samples(split)
        
    def _prepare_samples(self, split):
        """筛选指定split的样本"""
        samples = []
        for scene in nusc.scene:
            if scene['name'] in split_scenes[split]:
                sample_token = scene['first_sample_token']
                while sample_token != '':
                    sample = nusc.get('sample', sample_token)
                    samples.append(sample)
                    sample_token = sample['next']
        return samples
    
    def __getitem__(self, idx):
        sample = self.samples[idx]
        
        # 加载点云
        lidar_data = nusc.get('sample_data', 
                             sample['data']['LIDAR_TOP'])
        points = load_point_cloud(
            os.path.join(nusc.dataroot, lidar_data['filename']))
        
        # 获取标注框
        boxes = []
        for ann_token in sample['anns']:
            ann = nusc.get('sample_annotation', ann_token)
            box = nusc.get_box(ann_token)
            boxes.append(box)
        
        return points, boxes

4.2 数据增强策略

为提高模型鲁棒性,需要实施数据增强:

def apply_augmentation(points, boxes):
    """应用随机数据增强"""
    # 全局旋转
    angle = np.random.uniform(-np.pi/4, np.pi/4)
    rot_mat = np.array([[np.cos(angle), -np.sin(angle), 0],
                       [np.sin(angle), np.cos(angle), 0],
                       [0, 0, 1]])
    points[:, :3] = points[:, :3] @ rot_mat.T
    
    # 全局缩放
    scale = np.random.uniform(0.9, 1.1)
    points[:, :3] *= scale
    
    # 对每个框应用相同变换
    for box in boxes:
        box.rotate(Quaternion(axis=[0,0,1], angle=angle))
        box.scale(scale)
    
    return points, boxes

4.3 点云体素化处理

大多数现代3D检测器采用体素化输入:

from spconv.utils import VoxelGenerator

voxel_generator = VoxelGenerator(
    voxel_size=[0.1, 0.1, 0.1],
    point_cloud_range=[-50, -50, -3, 50, 50, 3],
    max_num_points=10,
    max_voxels=20000)

def points_to_voxels(points):
    """将点云转换为体素表示"""
    voxels, coords, num_points = voxel_generator.generate(points)
    
    # 归一化点特征
    voxel_features = np.concatenate([
        points[:, :3] - points[:, :3].mean(0),
        points[:, 3:]], axis=1)
    
    return {
        'voxels': voxels,
        'coordinates': coords,
        'num_points': num_points
    }

5. 3D目标检测模型实战

5.1 基于PointPillars的检测实现

PointPillars是兼顾精度和效率的经典3D检测架构:

import torch
import torch.nn as nn
from spconv import SparseConvTensor

class PointPillars(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        
        # Pillar特征网络
        self.pfn = nn.Sequential(
            nn.Linear(9, 64),
            nn.BatchNorm1d(64),
            nn.ReLU(),
            nn.Linear(64, 64),
            nn.BatchNorm1d(64),
            nn.ReLU()
        )
        
        # 2D CNN骨干网络
        self.backbone = nn.Sequential(
            nn.Conv2d(64, 64, 3, stride=2, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            # 更多卷积层...
        )
        
        # 检测头
        self.cls_head = nn.Conv2d(256, num_classes, 1)
        self.reg_head = nn.Conv2d(256, 7, 1)  # dx,dy,dz,w,l,h,θ
        
    def forward(self, voxel_features, coords, batch_size):
        # 1. Pillar特征提取
        pillar_features = self.pfn(voxel_features)
        
        # 2. 创建伪图像
        canvas = torch.zeros((batch_size, 64, 1200, 1200), 
                            device=voxel_features.device)
        canvas[coords[:,0], :, coords[:,2], coords[:,1]] = pillar_features
        
        # 3. 2D CNN处理
        features = self.backbone(canvas)
        
        # 4. 预测输出
        cls_pred = self.cls_head(features)
        reg_pred = self.reg_head(features)
        
        return cls_pred, reg_pred

5.2 损失函数设计

3D检测需要同时优化分类和回归任务:

def calculate_loss(cls_pred, reg_pred, gt_boxes, gt_labels):
    # 分类损失 (Focal Loss)
    cls_loss = FocalLoss()(cls_pred, gt_labels)
    
    # 回归损失 (Smooth L1)
    pos_mask = gt_labels > 0  # 正样本掩码
    reg_loss = SmoothL1Loss()(reg_pred[pos_mask], 
                             gt_boxes[pos_mask])
    
    # 方向分类损失
    dir_loss = CrossEntropyLoss()(dir_pred[pos_mask],
                                gt_directions[pos_mask])
    
    return cls_loss + reg_loss + dir_loss

5.3 模型训练流程

完整的训练循环实现:

def train(model, dataloader, optimizer, epochs=50):
    model.train()
    
    for epoch in range(epochs):
        for batch in dataloader:
            points, gt_boxes, gt_labels = batch
            
            # 体素化
            voxel_dict = points_to_voxels(points)
            
            # 前向传播
            cls_pred, reg_pred = model(
                voxel_dict['voxels'],
                voxel_dict['coordinates'],
                batch_size=len(points))
            
            # 计算损失
            loss = calculate_loss(cls_pred, reg_pred, 
                                gt_boxes, gt_labels)
            
            # 反向传播
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            
            print(f"Epoch {epoch} Loss: {loss.item():.4f}")

6. 检测结果后处理与评估

6.1 非极大值抑制(NMS)

from nms import rotate_nms

def apply_nms(boxes, scores, iou_threshold=0.3):
    """应用旋转框NMS"""
    keep_indices = rotate_nms(
        boxes.cpu().numpy(),  # [N,7] (x,y,z,w,l,h,θ)
        scores.cpu().numpy(),
        iou_threshold)
    
    return boxes[keep_indices], scores[keep_indices]

6.2 官方评估指标实现

Nuscenes使用独特的评估指标:

from nuscenes.eval.detection.evaluate import NuScenesEval

# 初始化评估器
nusc_eval = NuScenesEval(
    nusc,
    config_path='configs/detection_cvpr_2019.json',
    result_path='./results.json',
    eval_set='mini_val',
    output_dir='./eval_results')

# 运行评估
metrics = nusc_eval.main()
print("mAP: %.4f" % metrics['mean_ap'])

关键评估指标说明:

指标 说明 权重
mAP 平均精度(0.5-4.0m阈值) 40%
NDS 综合检测分数 60%
ATE 中心误差 包含在NDS中
ASE 尺寸误差 包含在NDS中
AOE 方向误差 包含在NDS中

6.3 结果可视化与分析

def visualize_detections(sample_token, pred_boxes):
    """可视化预测结果"""
    sample = nusc.get('sample', sample_token)
    lidar_data = nusc.get('sample_data', 
                         sample['data']['LIDAR_TOP'])
    
    # 渲染点云和预测框
    nusc.render_sample_data(lidar_data['token'],
                           override_boxes=pred_boxes)

典型问题分析技巧:

  1. 检查远处小物体检测效果
  2. 观察遮挡物体的召回率
  3. 分析方向预测准确性
  4. 检查尺寸预测的系统偏差
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐