nuScenes数据集实战:Python高效提取关键帧点云数据的全流程指南

1. 环境准备与数据加载

在开始处理nuScenes数据集之前,我们需要确保开发环境配置正确。首先安装必要的Python包:

pip install nuscenes-devkit pandas numpy open3d pyquaternion

创建项目目录结构时,建议采用以下组织方式:

/nuscenes_project
├── /data
│   ├── /sets
│   │   └── /nuscenes  # 官方数据集存放位置
├── /notebooks  # Jupyter实验笔记
└── /scripts    # 处理脚本

加载数据集的核心代码如下:

from nuscenes.nuscenes import NuScenes

# 初始化数据集接口
nusc = NuScenes(
    version='v1.0-mini',  # 或使用完整版'v1.0-trainval'
    dataroot='/path/to/data/sets/nuscenes',
    verbose=True
)

# 验证数据加载
print(f"数据集包含 {len(nusc.sample)} 个关键帧样本")

2. 关键帧点云数据结构解析

nuScenes采用多层数据结构组织传感器数据:

  • Scene: 20秒的连续驾驶片段
  • Sample: 0.5秒间隔的关键帧(含所有传感器数据)
  • SampleData: 单个传感器的具体数据记录

激光雷达数据存储在LIDAR_TOP通道,每个关键帧对应一个.pcd.bin文件。通过以下代码可获取首个样本的点云元数据:

first_sample = nusc.sample[0]
lidar_token = first_sample['data']['LIDAR_TOP']
lidar_data = nusc.get('sample_data', lidar_token)

print(f"点云文件路径: {lidar_data['filename']}")
print(f"时间戳: {lidar_data['timestamp']}")
print(f"坐标系: {nusc.get('calibrated_sensor', lidar_data['calibrated_sensor_token'])}")

关键数据结构关系如下图所示:

层级 描述 关联关系
Scene 20秒场景 包含40个Sample
Sample 关键帧 包含12个Sensor数据
SampleData 传感器数据 通过token关联具体文件

3. 点云数据提取核心技术

3.1 原始数据读取与解析

nuScenes使用二进制格式存储点云数据,每个点包含(x,y,z,intensity)四维信息。读取时需要特别注意坐标系转换:

import numpy as np

def load_point_cloud(nusc, sample_token, sensor='LIDAR_TOP'):
    """
    加载指定样本的点云数据
    返回:(N,4) numpy数组,包含x,y,z,intensity
    """
    sample = nusc.get('sample', sample_token)
    lidar_data = nusc.get('sample_data', sample['data'][sensor])
    pcl_path = os.path.join(nusc.dataroot, lidar_data['filename'])
    
    # 读取二进制文件
    scan = np.fromfile(pcl_path, dtype=np.float32)
    points = scan.reshape(-1, 4)
    
    # 转换到车辆坐标系
    cs_record = nusc.get('calibrated_sensor', lidar_data['calibrated_sensor_token'])
    points[:, :3] = np.dot(Quaternion(cs_record['rotation']).rotation_matrix, 
                          points[:, :3].T).T + np.array(cs_record['translation'])
    
    return points

3.2 批量提取优化技巧

处理大规模数据时,建议使用多进程加速:

from multiprocessing import Pool

def process_sample(args):
    nusc, sample_token = args
    return load_point_cloud(nusc, sample_token)

with Pool(8) as p:  # 使用8个进程
    point_clouds = p.map(process_sample, [(nusc, s['token']) for s in nusc.sample])

3.3 坐标系转换实战

nuScenes涉及三种主要坐标系:

  1. 传感器坐标系:原始数据坐标系
  2. 车辆坐标系:以车辆为中心的坐标系
  3. 全局坐标系:地图坐标系

转换示例代码:

from pyquaternion import Quaternion

def sensor_to_global(nusc, points, sensor_token):
    """
    将点云从传感器坐标系转换到全局坐标系
    """
    sd_record = nusc.get('sample_data', sensor_token)
    cs_record = nusc.get('calibrated_sensor', sd_record['calibrated_sensor_token'])
    pose_record = nusc.get('ego_pose', sd_record['ego_pose_token'])
    
    # 传感器->车辆
    points[:, :3] = np.dot(Quaternion(cs_record['rotation']).rotation_matrix, 
                          points[:, :3].T).T + cs_record['translation']
    
    # 车辆->全局
    points[:, :3] = np.dot(Quaternion(pose_record['rotation']).rotation_matrix, 
                          points[:, :3].T).T + pose_record['translation']
    
    return points

4. 常见问题解决方案

4.1 路径配置问题

当数据集路径与默认结构不符时,需要修改nuscenes-devkit源码中的路径解析逻辑。找到安装包中的nuscenes.py,约第100行处修改:

# 原代码
if not os.path.exists(self.dataroot):
    raise Exception("Dataroot does not exist: %s" % self.dataroot)

# 修改为自定义路径检查逻辑
custom_path = os.path.join(self.dataroot, 'custom_subdir')
if os.path.exists(custom_path):
    self.dataroot = custom_path
elif not os.path.exists(self.dataroot):
    raise Exception("Dataroot does not exist: %s" % self.dataroot)

4.2 内存优化策略

处理完整数据集时,可采用生成器模式逐帧处理:

def point_cloud_generator(nusc, sample_tokens):
    for token in sample_tokens:
        yield load_point_cloud(nusc, token)

# 使用示例
pc_gen = point_cloud_generator(nusc, [s['token'] for s in nusc.sample])
for pc in pc_gen:
    process_point_cloud(pc)  # 自定义处理函数

4.3 数据可视化技巧

使用Open3D进行交互式可视化:

import open3d as o3d

def visualize_point_cloud(points):
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(points[:, :3])
    
    # 强度值归一化用于着色
    colors = np.zeros((points.shape[0], 3))
    colors[:, 0] = points[:, 3] / points[:, 3].max()  # 红色通道表示强度
    pcd.colors = o3d.utility.Vector3dVector(colors)
    
    o3d.visualization.draw_geometries([pcd])

5. 高级应用与性能优化

5.1 点云特征提取

提取法向量和曲率等几何特征:

from sklearn.neighbors import NearestNeighbors

def compute_normals(points, k=30):
    nbrs = NearestNeighbors(n_neighbors=k).fit(points[:, :3])
    _, indices = nbrs.kneighbors(points[:, :3])
    
    normals = np.zeros((points.shape[0], 3))
    for i in range(points.shape[0]):
        cov = np.cov(points[indices[i], :3].T)
        U, S, Vt = np.linalg.svd(cov)
        normals[i] = U[:, -1]  # 最小特征值对应特征向量
    
    return normals

5.2 点云下采样策略

使用体素网格下采样保持几何特征:

def voxel_downsample(points, voxel_size=0.1):
    voxel_grid = {}
    for p in points:
        voxel = tuple((p[:3] // voxel_size).astype(int))
        if voxel not in voxel_grid:
            voxel_grid[voxel] = []
        voxel_grid[voxel].append(p)
    
    # 取每个体素中心点
    downsampled = np.array([np.mean(np.array(v), axis=0) 
                           for v in voxel_grid.values()])
    return downsampled

5.3 多传感器数据同步

实现激光雷达与相机数据对齐:

def map_pointcloud_to_image(nusc, points, sample_token, camera_channel='CAM_FRONT'):
    sample = nusc.get('sample', sample_token)
    cam_token = sample['data'][camera_channel]
    
    # 获取标定参数
    cam = nusc.get('sample_data', cam_token)
    cs_rec = nusc.get('calibrated_sensor', cam['calibrated_sensor_token'])
    
    # 点云->相机坐标系
    points_hom = np.concatenate([points[:, :3], np.ones((points.shape[0], 1))], axis=1)
    points_cam = np.dot(np.linalg.inv(Quaternion(cs_rec['rotation']).rotation_matrix), 
                       (points_hom[:, :3].T - cs_rec['translation'][:, np.newaxis])).T
    
    # 相机坐标系->图像平面
    K = np.array(cs_rec['camera_intrinsic'])
    points_img = np.dot(K, points_cam.T).T
    points_img = points_img[:, :2] / points_img[:, 2][:, np.newaxis]
    
    # 过滤超出图像范围的点
    valid = (points_img[:, 0] >= 0) & (points_img[:, 0] < cam['width']) & \
            (points_img[:, 1] >= 0) & (points_img[:, 1] < cam['height']) & \
            (points_cam[:, 2] > 0)  # z>0表示在相机前方
    
    return points_img[valid], valid

6. 实际项目中的经验分享

在处理多个自动驾驶项目后,我总结了以下关键经验:

  1. 数据校验必不可少:始终检查点云与标注的对应关系,使用nusc.render_sample_data()可视化验证

  2. 内存管理策略

    • 对于大规模处理,考虑使用memory-mapped文件
    • 将处理流程分解为多个阶段,中间结果保存到磁盘
  3. 性能瓶颈分析

    • 90%的处理时间消耗在数据加载和坐标系转换
    • 使用Numba加速关键计算部分
  4. 标注数据利用

def get_annotated_points(nusc, sample_token):
    sample = nusc.get('sample', sample_token)
    points = load_point_cloud(nusc, sample_token)
    
    # 初始化标注掩码
    annotations = np.zeros(points.shape[0], dtype=int)
    
    for ann_token in sample['anns']:
        ann = nusc.get('sample_annotation', ann_token)
        box = nusc.get_box(ann_token)
        
        # 转换到激光雷达坐标系
        box.translate(-np.array(ann['translation']))
        box.rotate(Quaternion(ann['rotation']).inverse)
        
        # 判断点是否在标注框内
        indices = box.points_in_box(points[:, :3])
        annotations[indices] = 1
    
    return points, annotations
Logo

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

更多推荐