简介:一份面向自动驾驶算法研发与数据研究人员的nuScenes数据集配套代码包,聚焦数据集的下载、devkit安装与结构查看,帮助快速上手复杂的多传感器数据与3D标注体系。压缩包体积仅8KB,共包含5个文件,涵盖Markdown与txt说明文档、HTML可视化页面、gitignore配置和inscode工程文件,便于轻量阅读与运行。目前已有120人学习浏览。内容围绕nuscenes-devkit库展开,覆盖场景、样本、样本数据、样本注释、实例、类别、属性、可见度、传感器、校准传感器、车辆姿态、日志和地图等核心模块,并涉及1000个驾驶场景、40万关键帧、140万张相机图片与1400万个3D标注框等数据规模信息。通过这份代码包,读者能够理解数据集的目录结构与对象标注体系,掌握数据可视化与分析的基本路径,为自动驾驶感知、预测等实验提供便捷的数据操作基础。
1. 为什么说 nuScenes 数据集是进阶自动驾驶绕不过去的一关
我最早做 3D 检测的时候,手里的数据还是几个相机加一组稀疏点云,觉得训练已经到头了。后来切到 nuScenes 数据集,第一感受是“数据量大了但代码也乱了”——一个 keyframe 就牵出六个相机、一组 32 线激光雷达和五个雷达,每个传感器还挂着自己的时间戳、标定参数和 ego 位姿。你如果不先把它背后的数据组织方式搞清楚,写出来的 dataloader 就是个时间黑洞。这个数据集能解决的核心问题很明确:想做多传感器融合、想做多目标跟踪、想做轨迹预测,或者只是想在真实路测之外找一个带地图语义标注的评测底座,它都是目前最常被拿来当基准的选择。它适合那些已经跑过 KITTI 或之类单目检测、但想知道“多模态数据到底怎么对齐”的从业者。本文会用可运行的代码把读取、坐标系变换和可视化串起来,并把最容易翻车的几个细节摆到台面上。
2. 先把数据集根目录读懂:maps、splits 与多模态数据家族
2.1 下载和版本选择:地图不是 trainval 自带的,漏下的人第一晚就翻车
打开 nuScenes 下载页面,你会看到一堆分开的压缩包。常见做法是下trainval主包,里面包含 metadata、samples 和 sweeps,但很多人忽略maps包和lidarseg包。我接触过的 A 同学第一次跑任何地图相关功能时,代码报错说找不到 map 文件,原因就是只下了 trainval。所以下载阶段就要想清楚:如果你的任务只用 3D检测,trainval + 对应 split 就够;如果后续要做地图辅助融合或者视觉落点查询,那maps包必须下;如果要做点云语义分割,lidarseg也要单独勾选。
下载完成后,打开根目录你应该看到这样几类内容:
| 子目录/文件 | 里面是什么 | 是否需要单独下载 |
|---|---|---|
samples/ | keyframe 传感器的数据文件,比如 LIDAR_TOP、CAM_FRONT、RADAR_FRONT 的 bin/jpg | 随主包 |
sweeps/ | 非 keyframe 的中间帧,雷达和相机都有 | 随主包 |
maps/ | 路网、车道、连接关系等地图语义文件 | 需另下 |
lidarseg/ | 逐点语义分割标注,按 sensor 和时间组织 | 需另下 |
v1.0-trainval/ | 所有 json 表,也就是数据集的索引和标注核心 | 随主包 |
记住一点:json 里的文件名都是相对dataroot的路径,所以下载后目录结构不能动。我习惯下载完先du -sh看一眼,如果splits和samples都存在但地图没有,先别急着写代码,回去补下地图包。
2.2 表结构里最值得先认识的五张表:从 scene 到 annotation 的时间轴线索
nuScenes 目录里真正的黑匣子不是 bin 文件,而是那堆v1.0-*.json。我第一次打印nusc.scene[0]的时候被大量字段搞晕了,后来整理出五张核心表,所有操作都围绕它们转。scene是一个 20 秒左右的连续片段;sample是 keyframe,频率约 2Hz;sample_data是某个 keyframe 下某个传感器的实际文件;sample_annotation是某个 keyframe 下的 3D 标注框;instance则是同一个物理目标跨多个 sample 的身份。
用代码看最直接:
from nuscenes.nuscenes import NuScenes nusc = NuScenes(version='v1.0-trainval', dataroot='/data/nuscenes') scene = nusc.scene[0] print(scene.keys()) print('sample数:', scene['nbr_samples'])这里nusc.scene[0]是从表里取第一行,它给出的first_sample_token和last_sample_token是时间轴的入口。NuScenes类初始化会加载所有表,所以后续通过nusc.get('sample', token)就能在任意表之间跳转。参数version决定读哪套 json,dataroot是根目录,两个都不能写错。我一般会在初始化后立刻打印一个 sample 的data字段,看看六个相机和雷达是不是都在。
2.3 读盘前先看 filename:为何 lidar 和 radar 的 bin 不能混着读
sample_data表里存了filename、fileformat、timestamp等字段。第一次写 loader 的人容易想当然:都是点云,都用np.fromfile读出来 reshape 不就行了?结果 lidar 能出图,radar 一跑就错。原因在两者的数据纬度和物理含义完全不同。
常见做法是先通过sd['fileformat']和sd['sensor']分流:
import os import numpy as np sd = nusc.get('sample_data', sample['data']['LIDAR_TOP']) path = os.path.join('/data/nuscenes', sd['filename']) print(sd['sensor'], sd['fileformat'], sd['width'], sd['height'] if 'height' in sd else '')输出里你会看到 LIDAR_TOP 通常是 bin,对应 3D 空间中的(x, y, z, intensity)四维点;而 RADAR_FRONT 的 bin 每行是 18 个通道,除了三维位置还包含径向速度、RCS 等信息。所以读取点云的正确姿势不是自己 reshape,而是用 devkit 里的LidarPointCloud和RadarPointCloud,这会在第三章展开。这里先记住:看到filename别直接np.fromfile,先判断 sensor 类型,否则雷达读成激光,出来的点云数量都对不上,后面所有坐标变换都白做。
3. 用 devkit 拉出第一帧多模态数据:从初始化到可视化
3.1 最小的初始化代码:先查 tables 再动手
我建议把初始化写成一个函数,每次实验只改 dataroot 和版本。注意NuScenes初始化会把所有 json 加载进内存,v1.0-trainval有上千个 scene,首次加载可能会等几秒,这是正常的。
from nuscenes.nuscenes import NuScenes dataroot = '/data/nuscenes' nusc = NuScenes(version='v1.0-trainval', dataroot=dataroot) scene = nusc.scene[0] sample = nusc.get('sample', scene['first_sample_token']) print(sample['timestamp']) print(list(sample['data'].keys()))sample['data']是一个字典,key 是传感器名,value 是对应sample_datatoken。这段代码只要跑通,说明 json 路径和文件组织没问题。如果sample['data']里缺少某个传感器,大概率是下载不完整,不要继续往下走。
3.2 读点云:LidarPointCloud 与 RadarPointCloud 不是一个量级
接下里读取一帧激光雷达和一颗前向雷达点云。这里不要自己写二进制解析,用 devkit 的类更稳:
from nuscenes.utils.data_classes import LidarPointCloud, RadarPointCloud lidar_sd = nusc.get('sample_data', sample['data']['LIDAR_TOP']) lidar_path = os.path.join(dataroot, lidar_sd['filename']) lidar_pc = LidarPointCloud.from_file(lidar_path) print('lidar points shape:', lidar_pc.points.shape) radar_sd = nusc.get('sample_data', sample['data']['RADAR_FRONT']) radar_path = os.path.join(dataroot, radar_sd['filename']) radar_pc = RadarPointCloud.from_file(radar_path) print('radar points shape:', radar_pc.points.shape)LidarPointCloud.from_file返回的点云矩阵是(4, N),四行分别是 x、y、z、intensity;RadarPointCloud.from_file返回(18, N),包含位置、多普勒速度等。这两个类都做了文件解析,比手写np.fromfile(..., dtype=np.float32)再猜 shape 可靠得多。需要注意dataroot和filename拼接时不要多写或漏写斜杠,我习惯用os.path.join而不是字符串加号。
3.3 手动做一条坐标变换链:sensor 到 ego 再到全局
多模态融合的核心是把 lidar 点云投到图像上,或把 radar 点云与 lidar 对齐。这需要把点从 sensor 自身坐标系转到 ego 车体,再转到全局坐标系。 devkit 的get_sample_data虽然能返回全局点云,但我建议你至少手动拆一次变换链,否则后续遇到真值组批、radar 投影这类问题会卡住。
from nuscenes.utils.geometry_utils import transform_matrix def sensor_to_global(nusc, sd): calib = nusc.get('calibrated_sensor', sd['calibrated_sensor_token']) ego = nusc.get('ego_pose', sd['ego_pose_token']) s2e = transform_matrix(calib['translation'], calib['rotation'], inverse=False) e2g = transform_matrix(ego['translation'], ego['rotation'], inverse=False) return e2g @ s2e s2g = sensor_to_global(nusc, lidar_sd) pts = lidar_pc.points[:3, :] pts_global = s2g[:3, :3] @ pts + s2g[:3, 3:4] print('points in global:', pts_global.shape)transform_matrix接受平移和旋转四元数,返回 4x4 齐次变换矩阵。两个矩阵相乘后,s2g就把 lidar 点送进了全局坐标系。这里最容易踩坑的是inverse参数,很多人把 sensor 到 ego 写反,结果所有点都在车底。建议先用一帧标注框和点云渲染做视觉验证,别直接拿去训练。
3.4 render_sample_data 出图:看标注比读数字快
当你把坐标系搞通后,最想确认的是“点云和 3D 框到底对不对”。这时直接用render_sample_data:
nusc.render_sample_data( sample['data']['LIDAR_TOP'], nsamples=3, underlay_map=False, with_anns=True, out_path='lidar_check.png' )这段代码会渲染 lidar 俯视图,并把该 keyframe 的标注框画出来。nsamples=3是说前后扩展几帧,with_anns控制是否画标注框,out_path保存图片。不同版本 devkit 的参数名可能有差异,如果报underlay_map不存在,直接去掉即可。我第一次跑这个函数时,所有 3D 框都偏离点云一大截,追了半天下载包,最后发现是dataroot配错,导致转出来的 ego pose 全不对。所以第一帧一定要出图确认,这是性价比最高的检查。
4. nuScenes 处理中常见的 5 个坑:从时间戳对齐到 eval 分数骗人
4.1 坑1:radar 点云噪声大,不裁剪就喂模型只会让人血压高
现象:雷达点云直接可视化时,车前方有大量散乱点,某些点明显远离地面。 原因:雷达在复杂城市环境中有多径反射和地面杂波,且标注真值只针对动态目标,静态点会大量保留。 解决:在进模型前做一次范围裁剪,只保留自车前后左右一定范围内的点,再结合高度过滤。常见做法是以自车为中心裁剪x, y到[-70m, 70m],z到[-3m, 3m]。如果你用 devkit 的RadarPointCloud.from_file读原始点,必须在 transform 之前处理,因为它输出的是 sensor 坐标系。
4.2 坑2:sample_data 时间和标注时间不是一个时钟
现象:用一段连续帧做检测输入时,发现物体位置和图像时间对不上,动态车辆明显滞后。 原因:sample表的 timestamp 是 keyframe 的主时钟,而sample_data表的 timestamp 是传感器实际采集时刻。相机和 lidar 硬件触发有延迟,尤其 radar 与 lidar 之间的时间偏移可达几十毫秒。 解决:所有多模态读取必须以传感器的sd['timestamp']为基准,不要拿sample['timestamp']统一对齐。我通常会写一个映射表,把每个传感器的 token 和它的绝对时间戳存成数组,再按时间插值 ego pose。
4.3 坑3:ego_pose 插值用最近邻会翻车
现象:车辆高速转向时,点云投影到相机图上歪得离谱。 原因:ego_pose的采样率有限,最近邻插值在快速加减速场景会引入明显位姿误差。 解决:用线性插值或四元数球面插值,按相对时间差把位姿插到传感器时间点。代码上不要自己手写 step 函数,而是对前后两帧ego_pose做插值。如果嫌弃麻烦,get_sample_data内部已经帮你做了部分处理,但底层逻辑仍然是最近邻,所以高危场景最好自己实现。
4.4 坑4:lidarseg 需要单独下载,trainval 目录里没有
现象:按照教程初始化nusc.lidarseg时出现 KeyError。 原因:lidarseg的逐点语义标注没有随 trainval 主包发布,官网下载页里是独立选项。 解决:回到下载页确认是否勾选了 lidarseg,下载后放在 dataroot 下,保持 json 里指向的路径不变化。之后通过nusc.get('lidarseg', sample_data_token)取某一帧的逐点标签,注意它的 token 体系和 sample 不一样。
4.5 坑5:eval 的 min_overlap 按类别分开调,用一个默认值会误杀大车
现象:自己测试时 mAP 挺好看,提交官方 eval 后分数血崩。原因:检测评估里面min_overlap是匹配阈值,大车和卡车要求更高的 IoU,小车可以放宽。直接用一套默认阈值会让大车漏配,FP 暴增。解决:按类别设定min_overlap映射表,eval 配置里把小车、卡车、行人分开配置。我一般先跑 10 帧样例看各类别的 TP/FP 分布,再决定阈值,而不是一次全量评估。
5. 进阶:把 single frame 改成 temporal slice,给模型补上时间维度
5.1 拉一段 0.5s 的 sweep 序列,在时间上对齐
单帧点云在遮挡严重时经常缺目标,常见做法是沿prev链拉出一段历史雷达帧,合并成时间切片。它的价值是让检测器能利用物体过去的运动轨迹,让静止物体的点更密集。代码可以这样写:
def build_temporal_lidar(nusc, sample_token, hist=10): sample = nusc.get('sample', sample_token) lidar_sd = nusc.get('sample_data', sample['data']['LIDAR_TOP']) clouds = [] timestamps = [] sd = lidar_sd for i in range(hist + 1): pts, _, _ = nusc.get_sample_data(sd['token']) clouds.append(pts[:3, :]) timestamps.append(sd['timestamp']) if sd['prev'] == '': break sd = nusc.get('sample_data', sd['prev']) points = np.concatenate(clouds, axis=1) rel_ts = np.array(timestamps) - timestamps[0] return points, rel_tsnusc.get_sample_data对 lidar token 返回的已经是全局坐标点云,所以合并到同一坐标系下是安全的。hist=10表示当前帧加前 10 帧,约 0.5 秒。rel_ts是相对第一帧的时间差,后续喂给网络时可以作为 extra feature。如果sd['prev']为空说明到了 keyframe 序列头部,break 掉即可。
5.2 检查你的时间窗选对了没有
时间切片不是越长越好。我习惯把合并后的点云点数、时间间隔先打印出来,确认雷达帧之间是等间隔,再做成输入。如果rel_ts出现重复值,说明两条记录指向同一帧,多半是 token 链没跳对。另一个检查手段是把合并点云按时间着色,直接画成 scatter 图,看看运动目标是否呈现拖尾。这个验证只花两分钟,能避免把时间切片做成一坨无序点云的黑匣子。我自己吃过一次亏,当时把prev和next方向写反,所有 temporal 特征都是未来帧,模型在验证集上指标简直完美,一上实车就失灵。后来每次拉时间序列前都先打印timestamps顺序,成了固定习惯。
这也是我想强调的最后一件事:nuScenes 代码本身不难,难的是你始终要怀疑“坐标系、时间戳、传感器类型”这三件事。每次写读取逻辑,先出图、先打印形状、先打印时间差,再做训练。希望帮到你。
本文还有配套的精品资源,点击获取