2026/10/18 5:40:46

nuScenes数据集深入解析:多模态自动驾驶数据读取与坐标系变换

nuScenes数据集深入解析:多模态自动驾驶数据读取与坐标系变换 简介面向自动驾驶研究与开发者这份代码包围绕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 检测、轨迹预测都够用。模态数量原始格式使用时的常见形态相机图像6jpg1600×900 RGB训练常缩放激光雷达1pcd约 3 万点/帧含 x/y/z/intensity毫米波雷达5pcd点稀疏带速度/多普勒信息高精地图全局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(versionv1.0-mini, dataroot/data/nuscenes, verboseFalse) 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( versionv1.0-mini, dataroot/data/nuscenes, verboseTrue, )初始化后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_tokensNone) 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×900mobilenetv2 默认输入是 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, dtypenp.float32).reshape(-1, 18) # 常见处理去掉无效速度点 valid points[:, 8] ! 0 # 依数据版本略有差异取有效检测标识 points points[valid]这段代码示意性读取 radar 点云的 18 维原始特征并过滤掉无效测量。逻辑说明radar 每点包含位置、速度、RCS、多普勒状态等多个字段具体列索引在不同版本里略有差别代码里我必须提醒你先用官方开源预处理脚本查看字段名不要凭记忆硬编码列号。参数说明过滤条件可以放宽到“动态目标且速度有效”这样后续做速度预测时不会引入一堆静止杂点。用 radar 做融合的常规方案是先把 5 个雷达的点统一转到 ego 坐标系再与 lidar 点云做空间对齐按体素或距离融合。这个流程跑通后模型对远距离小物体的召回会有肉眼可见提升。4. 常见问题排查同一份数据为什么你就是读不出来 / 崩 / 错位4.1 初始化后 sample 列表为空版本字符串写错现象NuScenes(versionv1.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 都值。希望这些踩坑经验能帮到你少走点弯路。本文还有配套的精品资源点击获取