简介:面向自动驾驶研究与开发者,这份代码包围绕nuScenes数据集的下载、安装与可视化分析展开,完整梳理了1000个复杂场景、40万关键帧、140万相机图、1400万3D标注框以及多传感器配置和23类目标对象的处理逻辑,可帮助中高级算法工程师与研究生快速搭建数据理解框架,减少入门阶段的踩坑。资源共5个文件,以md学习笔记为核心,HTML可视化浏览页辅助展示,inscode记录工程配置,txt管理Python依赖,gitignore服务版本管理,压缩包仅8KB,虽体积小巧但组织清晰,适合按目录对照学习。已有119人学习下载。内容上提供了nuscenes-devkit的常用调用思路,涵盖场景、样本、样本数据、样本注释、传感器、标定信息、车辆姿态等内容的读取与展示,并附有安装、查看数据集的要点说明,使用者可据此直接上手实验,为后续三维目标检测与自动驾驶模型训练备好数据工具链。
1. 深入 nuScenes 数据集:先把这批自动驾驶数据盘的逻辑弄清楚
做过多模态自动驾驶感知的人,大多绕不开 nuScenes 数据集。它不是简单的“图片 + 标注”压缩包,而是把 6 路相机、1 个激光雷达、5 个毫米波雷达、高精地图和时间对齐后的标注放在同一套索引体系里,让你一次性面对“传感器数据 + 3D 标注 + 时序”三个难题。很多团队第一次跑通官方示例代码后,以为训练马上能开始,结果在坐标变换、数据版本、雷达去噪上耗掉两周,最后发现问题出在“读数据的姿势”而不是模型。这篇文章我按自己实际调数据的过程,把 nuScenes 从结构、官方代码、常见翻车点到进阶验证讲一遍,适合刚准备用这个数据集的读者,也适合已经在跑但被坐标系折磨的选手。
2. 数据这么切,模型才不白训:nuScenes 的数据结构与标注体系
2.1 传感器套件与采样节奏:一个 sample 背后有哪些模态
nuScenes 的采集车装了一套很标准的自动驾驶传感器组合:车顶一个 32 线激光雷达(LIDAR_TOP),车周围分布 6 个相机(CAM_FRONT、CAM_FRONT_LEFT、CAM_FRONT_RIGHT、CAM_BACK_LEFT、CAM_BACK_RIGHT、CAM_BACK),另外还有 5 个毫米波雷达,分布在车头和四个角。最核心的设计是“关键帧 sample”概念:每 0.5 秒(2Hz)采一次同步快照,把当时所有传感器读数、对应的标注、位置姿态全部打包成一个 sample。这个节奏比很多纯视觉数据集慢,但换来的是激光雷达点云足够密、标注质量足够高,训练 3D 检测、轨迹预测都够用。
| 模态 | 数量 | 原始格式 | 使用时的常见形态 |
|---|---|---|---|
| 相机图像 | 6 | jpg | 1600×900 RGB,训练常缩放 |
| 激光雷达 | 1 | pcd | 约 3 万点/帧,含 x/y/z/intensity |
| 毫米波雷达 | 5 | pcd | 点稀疏,带速度/多普勒信息 |
| 高精地图 | 全局 | json + 二值图 | 车道、路沿、停车位分层 |
整个数据集的完整版包含约 1000 个场景、140 万+ 个 3D 标注框,很多人第一次上手根本不需要把全量数据下完,官方提供的 mini 版只有 10 个场景,几百个样本,足够把代码链路跑通。我自己的习惯是:先用 mini 把“读取 → 可视化 → 做 dataloader”全流程跑顺,再决定要不要下载 trainval 全量。因为全量数据解压后体积大,如果脚本里有坐标错位这种低级 bug,反复重下数据很浪费时间。
2.2 从 scene 到 sample_data:官方 API 的索引路径
nuScenes 的数据结构是严格分层的:最外层是 scene(一段连续驾驶日志),scene 下面挂了若干个 sample(关键帧),每个 sample 又通过 data 字段索引到各传感器的 sample_data,也就是实际存在的图像、点云文件。每个 sample_data 又关联两个关键对象:calibrated_sensor 记录该传感器相对车辆坐标系的外参和相机内参,ego_pose 记录车辆在地图里的位置和朝向。整套结构像一棵树,官方 SDK 里 get_sample_data 这类常用 API 都是沿着这棵树做索引和坐标变换。
实际在磁盘上,文件路径和 json 里的 token 是对应的。比如某个相机图像会放在 samples/CAM_FRONT/n008-2018-08-01_15-19-01-0402__CAM_FRONT__xxxxxxxx.jpg,点云放在 samples/LIDAR_TOP 下。代码里想要拿到某个 sample 的数据,先按 token 去 nusc.sample 里查,再取 sample['data']['LIDAR_TOP'] 这类 key。这个机制本身不复杂,但它带来的常见坑是:只记住文件路径,忘记加载 calibrated_sensor 和 ego_pose,结果点云坐标完全乱套。
2.3 真值标注与类别体系:25 类边界框怎么查
nuScenes 的 3D 标注存在 instances 和 sample_annotation 两个层面:一个 instance 代表一个被持续追踪的物体,在连续多帧里不断出现;sample_annotation 则是这个物体在某一帧的具体状态,包含 3D 边界框的中心、尺寸、朝向、属性、遮挡程度和难例标记。官方把所有标注对象归类为 25 个类别,比如小汽车、卡车、行人、自行车、摩托车、交通锥、障碍物等。查询类别时不需要自己解析 json 全字段,直接遍历 nusc.sample_annotation 就能拿到:
from nuscenes import NuScenes nusc = NuScenes(version='v1.0-mini', dataroot='/data/nuscenes', verbose=False) categories = {cat['name'] for cat in nusc.category} print(categories)这段代码先加载数据集索引,再用集合推导式把所有出现的类别名去重打印。逻辑很简单,但有两个值得注意的点:第一,类别名之间不是全靠 category 判断,很多标注还带 instance 字段,真要统计某个物体的连续轨迹链,需要串联 instance token;第二,3D 框的朝向用的是四元数而不是欧拉角,直接打印 rotation 会看到四个数,很多新人拿 yaw 去换算时算错方向,后面我会专门讲这个坑。做数据分布分析时,我一般先用这段脚本统计每类框的数量、尺寸分布,再决定模型头怎么设计,而不是盲目套开源模型。
3. 用官方 devkit 跑通最小示例代码:从下载目录到点云+图像渲染
3.1 装好环境与依赖:这些隐式依赖别漏
nuScenes 官方提供 nuscenes-devkit 这个 Python 包,封装了数据装载、可视化、真值提取等功能,这也是标题里那个“代码”最直接的入口。安装本身不难,一条 pip 命令就能完成,但它会带上一些隐式依赖,比如 pyquaternion 用来处理旋转、pypcd 用来读点云、pyyaml 管理配置。如果只装主包不装依赖,后面跑渲染脚本时经常报 module not found。
python -m pip install nuscenes-devkit python -m pip install pyquaternion pypcd pyyaml逻辑说明:第一行装的是 devkit 本体,第二行把三个高频依赖补齐。参数说明里有一个关键点:pypcd 在不同系统上有兼容性问题,Windows 下常出现编译失败;如果你主要在 Windows 上做原型验证,更省事的做法是直接把 pcd 文件用 open3d 读取,num_points 等信息用 pypcd 的替代路径解析。看完官方示例代码就知道,devkit 负责的是“把数据索引到路径、把坐标系统一、把真值转成 API 对象”,真正吃数据的部分仍然要自己写。
3.2 加载数据集实例:路径、版本、内存的取舍
初始化 NuScenes 类是整个流程的第一步,也是最容易出错的一步。构造参数里 dataroot 是数据根目录,version 是数据版本字符串,这两个组合错了就会得到一整套空数据。常用版本有 v1.0-mini、v1.0-trainval、v1.0-test,我用 mini 跑通流程时经常发现有人把 version 写成 trainval,但 dataroot 目录里实际下载的是 mini,导致所有 sample 列表为空。
from nuscenes import NuScenes nusc = NuScenes( version='v1.0-mini', dataroot='/data/nuscenes', verbose=True, )初始化后,verbose 会打印加载的 scene 数量、sample 数量和 category 数量。逻辑说明:NuScenes 类在初始化时会把 json 索引全部读进内存,包括 scene、sample、sample_data、instance、calibrated_sensor 等所有表,因此全量数据集初始化后会占不少内存,但 mini 几乎无感。参数说明里我建议把 dataroot 写绝对路径,别写相对路径;训练脚本里经常用相对路径启动,一旦工作目录切错,几千个文件路径全部失效,且报错发生在读取阶段而不是初始化阶段,排查起来很绕。
3.3 按 sample 取数:拿到图像、lidar、radar 和标注
拿到 NuScenes 对象后,核心操作是“给一个 sample token,取回该帧所有传感器的数据路径和真值”。官方给出的一行 call 就是 get_sample_data,它同时返回数据文件路径、3D 框列表、以及相机内参。以相机图像为例,代码很小,但搞懂里面发生了什么很重要:它先根据 sample_data token 找到传感器姿态和外参,再把真值框从全局坐标系转到当前传感器坐标系,最后把修改后的 box 对象带回来。
sample = nusc.sample[0] sample_token = sample['token'] cam_token = sample['data']['CAM_FRONT'] data_path, boxes, camera_intrinsic = nusc.get_sample_data(cam_token, selected_3d_tokens=None) print(data_path) print(len(boxes)) print(camera_intrinsic)这段代码返回了 CAM_FRONT 图像路径、投影到该相机视角下的 3D 框集合、以及相机内参矩阵。逻辑说明:boxes 已经是经过外参变换后的对象,你可以直接拿它的 corners 画到图像上,不需要再手动乘投影矩阵。参数说明:selected_3d_tokens 传 None 表示返回该帧所有标注框;如果你只要某几个物体的框,可以传入 instance token 列表,能省掉后续筛选步骤。很多开源项目在 dataloader 里每次都这样取一次路径,然后自己在 getitem 里再读图,这就引出 3.4 的坐标变换问题:get_sample_data 给的是“从全局到该相机”,而点云想映射到图像要另外算。
图像分支如果要快速换 backbone 做对比实验,常见做法是直接改一段 mobilenetv2 代码当特征提取器,但白嫖预训练权重之前一定要先看输入尺寸:nuScenes 图像是 1600×900,mobilenetv2 默认输入是 224×224,缩放策略和图像裁剪位置直接决定小目标(行人、锥桶)能不能看见。
3.4 把 lidar 和图像对齐到同一坐标系:外参、内参、时间戳
3D 点云要投影到图像上,需要三步:把 lidar 点从 lidar 自身坐标系转到车辆坐标系(用到 calibrated_sensor 的 translation/rotation),再把车辆坐标转到全局坐标(用到 ego_pose),最后用相机外参和内参投影到像素平面。换个方向也一样:真值框从全局转到某个相机,会先转到车辆再转到该相机。这里最常见的错误是少转一次坐标系,导致投影结果偏得离谱还查不出原因。
import numpy as np from pyquaternion import Quaternion lidar_sample_data = nusc.get('sample_data', sample['data']['LIDAR_TOP']) lidar_calib = nusc.get('calibrated_sensor', lidar_sample_data['calibrated_sensor_token']) ego_pose = nusc.get('ego_pose', lidar_sample_data['ego_pose_token']) # lidar 点云读取后是 Nx3,先补一维齐次坐标 # 该变换把 lidar 坐标转到车辆坐标系 def lidar_to_ego(points): T = np.eye(4) T[:3, :3] = Quaternion(lidar_calib['rotation']).rotation_matrix T[:3, 3] = lidar_calib['translation'] return (T @ points.T).T这段代码先把 lidar 坐标系下的点转到 ego 车辆坐标系。逻辑说明:Quaternion 的 rotation_matrix 返回 3×3 旋转矩阵,translation 是三维平移,组合成 4×4 齐次矩阵后,左乘点云即可。参数说明:这里只做了 lidar→ego,后面再乘 ego_pose 的外参矩阵才能得到全局坐标,而要把全局坐标投到图像,还需要相机侧的 calibrated_sensor 和相机内参。这些步骤很多教程一句话带过,实际上错一个符号就全错,特别适合在跑第一个例程时逐行验证中间矩阵的值。
做完坐标变换后,强烈建议先渲染一张带 3D 框的图片看一眼:框的边角是不是贴合物体、前后遮挡对不对。如果框整片偏到车的左侧,十有八九是旋转矩阵转置写反;如果只在图像边缘偏,往往是时间戳对齐问题。数据里所有的 sensor 有自己的时间戳,查询时要用 sample_data 的对应关系,不要擅自拿另一帧的 pose 替换。
3.5 毫米波雷达点云:多普勒信息不要丢
nuScenes 里 radar 点云只保留稀疏点,每帧每个雷达大概几百个点,5 个雷达合起来也就一两千。很多新手拿到 radar 点云后直接扔给模型做输入,效果比只用 lidar 还差,原因通常有两个:一是没有做动态目标过滤,二是没有利用多普勒速度。radar 的 pcd 文件里带速度相关字段,可以区分静止目标和运动目标,这对轨迹预测、速度估计很有用。
import numpy as np points = np.fromfile(pcd_path, dtype=np.float32).reshape(-1, 18) # 常见处理:去掉无效速度点 valid = points[:, 8] != 0 # 依数据版本略有差异,取有效检测标识 points = points[valid]这段代码示意性读取 radar 点云的 18 维原始特征,并过滤掉无效测量。逻辑说明:radar 每点包含位置、速度、RCS、多普勒状态等多个字段,具体列索引在不同版本里略有差别,代码里我必须提醒你:先用官方开源预处理脚本查看字段名,不要凭记忆硬编码列号。参数说明:过滤条件可以放宽到“动态目标且速度有效”,这样后续做速度预测时不会引入一堆静止杂点。用 radar 做融合的常规方案是先把 5 个雷达的点统一转到 ego 坐标系,再与 lidar 点云做空间对齐,按体素或距离融合。这个流程跑通后,模型对远距离小物体的召回会有肉眼可见提升。
4. 常见问题排查:同一份数据,为什么你就是读不出来 / 崩 / 错位
4.1 初始化后 sample 列表为空:版本字符串写错
现象:NuScenes(version='v1.0-trainval', dataroot='/data/nuscenes') 初始化成功,但 nusc.sample 为空,打印 scene 数量为 0。
原因:dataroot 目录下实际只解压了 v1.0-mini,但代码里 version 参数写的是 v1.0-trainval。devkit 只按字符串去查对应 json 目录,不会主动检测数据版本,所以不报错,直接返回空。
解决:优先把 version 改成与目录实际命名一致。检查 dataroot 下是否能看到 v1.0-mini 或 v1.0-trainval 这样的文件夹,然后同步修改代码。先确认目录树再做其他排查,能省很多无用功。
4.2 点云投影到图像上方向错乱:坐标系少转一次或矩阵转置写反
现象:lidar 点云投影后落在图像上,但形状像“翻面了”,物体轮廓完全错位;某个方向的点跑到相反方向。
原因:常见的两种:一是把 ego 坐标当成全局坐标直接投到相机,少乘一次 ego_pose;二是外参旋转矩阵的行列安排写反,numpy 的坐标轴顺序和 pyquaternion 返回的矩阵默认布局不一致。
解决:把所有点云统一先转全局,再从全局转到目标传感器;如果你自己写外参矩阵,把 lidar_to_ego、ego_to_global、global_to_cam、cam_to_pixel 每一步都先用简单坐标点(如原点、单位向量)验证一遍,再做批量投影。我一般会在代码里加一个调试开关,输出每一步变换前后的坐标,看数值是否符合直觉。
4.3 加载慢、内存涨得快:真值盒子列表重复计算
现象:训练 dataloader 里每 epoch 都调用 get_sample_data,训练速度越来越慢,内存占用线性上升。
原因:get_sample_data 内部会对该帧所有 box 做一次坐标变换,如果每个 step 都重新加载并变换几千个框,CPU 开销会吃掉训练瓶颈;更糟的是有人把 nusc.get_sample_data 返回的 boxes 存进全局列表,造成真值对象越堆越多。
解决:在训练前把每个 sample 对应的 box 路径和坐标预计算并缓存,dataloader 只读缓存;如果确实需要在训练中在线变换,用 numba 或 numpy 批量处理,避免 Python 层逐框循环。先按项目里的代码规范把变量命名和缓存逻辑理清楚,这个瓶颈一般当天就能暴露。
4.4 radar 点云做输入后模型反而变差:没做多普勒过滤
现象:融合了 radar 点云的模型在 val 上 AP 比纯 lidar 低 3-5 个点,尤其在高速场景下误检变多。
原因:radar 数据里包含大量静态目标和无效多普勒点,直接把 raw 点塞进网络,等于给模型加了大量噪声输入。
解决:读取 pcd 后先过滤静止点,保留动态目标;如果模型需要速度特征,把多普勒速度字段作为额外通道输入,而不是只喂 x/y/z。采样时还要注意 5 个 radar 的分布,合并前统一坐标系,否则会看到同一个目标被重复检测成两三个框。
4.5 真值框的旋转角好像总差 90 度:四元数转 yaw 时踩坑
现象:可视化时 box 的朝向与车辆实际行驶方向不符,检测网络输出的角度也系统性偏移。
原因:3D 框的 rotation 字段是四元数,不是欧拉角。直接用前三项或者只取四元数最后一个分量当 yaw,结果自然不对。
解决:用 pyquaternion 的 Quaternion(rotation).yaw_pitch_roll 取 yaw;或者先归一化四元数,再按标准公式转欧拉角。官方 devkit 的 Box 类内部已经处理了这部分,尽量直接用 Box 对象,不要自己去解析列表。
5. 进阶验证技巧:用一小段渲染脚本把模型输出和真值对齐看
训练深度学习模型,尤其是 3D 检测,最怕的是 loss 在降、但可视化结果不忍直视。nuScenes 提供了一套快速验证工具,你可以写个几十行的渲染函数,循环几个 sample 把模型的预测 box 和真值 box 同时画在图像上,看对齐程度。这个方法比盯 loss 曲线更直观,也最能暴露外参、时间戳、坐标系层面的隐性 bug。
from nuscenes.utils.data_classes import Box from nuscenes.utils.geometry_utils import transform_matrix import cv2, numpy as np def draw_boxes_on_image(sample_token, boxes_pred): cam_token = nusc.get('sample', sample_token)['data']['CAM_FRONT'] img_path, boxes_gt, cam_intrinsic = nusc.get_sample_data(cam_token) img = cv2.imread(img_path) for box in boxes_gt: corners = box.corners()[:, [0, 3, 6, 7]] # 投影并画线(示意) for c in corners.T: cv2.circle(img, (int(c[0]), int(c[1])), 2, (0, 255, 0), -1) return img这段代码思路是用 get_sample_data 拿到真值框,再把预测框同样转到相机坐标系,最后统一画在图像上做对比。逻辑说明:预测框一般要先经过和真值相同的坐标变换,或者直接把模型输出的全局坐标 box 交给 devkit 的 Box 类处理。参数说明:这里只画了四个角点,实际验证可以把 8 个角点全画出来,再连成立方体,能更清楚看到朝向误差。
每次改模型结构或数据处理流程,我都会留这套可视化脚本,跑 5 个 mini 场景,肉眼检查预测框和真值框在图像上的贴合度。以前我拿到新数据不先看坐标系,结果调了一个月训练流程才发现是外参没对齐,白白浪费时间。先把 nuScenes 的数据链路跑明白,比多训十个 epoch 都值。希望这些踩坑经验能帮到你,少走点弯路。
本文还有配套的精品资源,点击获取