news 2026/9/2 22:53:56

nuScenes数据集代码拆解:从数据表关联到3D框投影与点云变换

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
nuScenes数据集代码拆解:从数据表关联到3D框投影与点云变换

简介:面向自动驾驶算法研究者的nuScenes数据集配套代码包,聚焦复杂驾驶场景下多传感器数据的获取、解析与可视化。资源围绕nuScenes数据集的1000个20秒场景、40万关键帧及1400万3D标注框等核心内容,给出基于nuscenes-devkit库的操作示例,帮助开发者快速上手传感器数据、样本注释和地图等模块的读取与分析。压缩包共5个文件,包含代码入口、依赖清单、环境配置说明与使用文档,整体仅8KB,轻量易用。已有118人学习下载,适合正在学习自动驾驶数据集处理、需要快速理解nuScenes结构与devkit用法的初中级开发者。通过源码可直观掌握场景样本、校准传感器、车辆姿态等对象的调用方式,并以此为起点扩展自己的数据挖掘与可视化脚本,降低入门成本。 做自动驾驶感知的同学对nuScenes数据集应该不陌生,但很多人卡在“数据下载好了,却不知道代码怎么读”这一步。这个数据集包含了6个相机、5个毫米波雷达和1个激光雷达的同步数据,加上地图信息和23类3D标注框,是当前多模态3D检测和BEV感知绕不开的benchmark。这篇文章我拿实际跑过的代码来拆一遍,从数据目录结构、标注表关联、坐标系变换到3D框投影到图像、雷达点云读取,全程带可运行示例,适合刚开始接触nuScenes、准备复现模型或者想把数据链路搞明白的工程师参考。

我最早也被一堆JSON文件名劝退过,什么sample、sample_data、sample_annotation、calibrated_sensor、ego_pose,光看文档完全理不清。后来耐着性子把数据表的关联一个个打出来,才意识到这套设计的逻辑其实很清晰。下面我按自己项目里的推进顺序来写,尽量把踩过的坑和验证方法都带出来。

1. 项目定位与代码拆解思路

1.1 为什么选择nuScenes做多模态感知

做自动驾驶感知,候选数据集其实不少:KITTI是单目+64线激光雷达,数据量小,适合快速验证;Waymo Open Dataset规模大但传感器配置较重,数据格式和工具链上手成本高。nuScenes的定位比较居中,传感器配置完整,关键帧是2Hz,每个20秒的scene有40个关键帧,加上中间的sweeps,覆盖了城市、郊区、夜晚、雨天多种场景。它的另一个优势是官方提供了nuScenes-devkit这个Python包,虽然不是十全十美,但至少把数据读取和坐标系变换的脏活做了大半。

训练一个3D检测模型之前,首先要确保训练样本能正确对齐。nuScenes的相机、激光雷达、毫米波雷达都做了时间同步和标定,每一条sample_data都有对应的calibrated_sensor和ego_pose,这意味着代码层面可以比较轻松地把不同模态的数据变换到同一坐标系下。这也是我选择它来做多模态感知项目的原因,数据底子干净,代码侧能少操很多心。

1.2 代码层面的三个推进阶段

我在项目里把nuScenes的代码工作分成三个阶段。第一阶段是“能读”,把数据目录和JSON标注表的结构摸清楚,知道哪张表记录什么信息;第二阶段是“能算”,把坐标系变换链弄明白,不管是把3D框投影到图像,还是把点云从传感器坐标转到自车坐标,都要手写出来并验证;第三阶段是“能用”,把这些读取逻辑封装成Dataset类,供PyTorch训练循环调用。

这篇文章会覆盖前两个阶段,第三个阶段涉及具体模型训练,不同模型差异比较大,不太适合一概而论。但前两步做扎实了,第三步基本就是套模板的事。

2. 数据底层结构:目录、标注表与坐标系

2.1 samples和sweeps的区别

nuScenes的数据目录里有两个容易混淆的文件夹:samples和sweeps。简单说,samples是每个关键帧时刻的所有传感器数据,一个sample对应一次完整的“同时刻采集”,频率是2Hz;sweeps则是两个关键帧之间其他时刻的数据,主要用于tracking或者利用历史信息增强当前帧。

理解这个区别对代码调试很重要。我在早期写代码时,直接用sweeps里的点云去和samples里的标注框对齐,结果发现时间戳对不上,框和点云总是差一段距离。后来才意识到,标注框只标在关键帧时刻的传感器数据上,sweeps里的数据只是辅助信息,不能直接与samples里的标注一一对应。

2.2 核心标注表的关联关系

nuScenes的标注数据分布在多张JSON表里,最核心的几张是scene、sample、sample_data、sample_annotation、instance、category。

它们之间的关系可以这样理解:一个scene是整个视频片段,包含多个sample;每个sample是一个关键帧时刻,通过sample['data']字段可以拿到该时刻所有传感器的sample_data token;每个sample_data对应一个具体的传感器文件,比如相机图片或雷达点云。sample_annotation记录的是某个sample时刻某个目标实例的3D标注框,它通过instance关联到类别,通过sample关联到具体时刻。

用代码查这张关联链最直观。比如要找到某个scene第一个sample的相机前视图标注框,代码路径是scene → first_sample_token → sample → data.CAM_FRONT → sample_data → sample_annotation。这种链式查询一开始不太习惯,但好处是结构清晰,任何一环都可以用token精确定位,不会出现跨表字段冗余的问题。

2.3 坐标系与位姿变换链

坐标系是nuScenes代码里最容易出错的地方。整个数据集涉及三套坐标系:global坐标系是全局地图坐标;ego坐标系以自车为中心,x向前、y向左、z向上;sensor坐标系以传感器自身为原点。

每一帧sample_data都记录了calibrated_sensor_token和ego_pose_token。calibrated_sensor存的是传感器相对于ego的平移和旋转,ego_pose存的是ego相对于global的平移和旋转。所以要把传感器坐标系下的点变换到global坐标系,变换链是sensor → ego → global;反变换就是global → ego → sensor。

很多教程直接调用nusc.get_sample_data接口拿投影结果,这当然快,但如果不理解背后的变换链,一旦结果不对就不知道怎么排查。我在实际项目中是先把这条链路手写出来,跑通了再换官方接口,这样出了问题能一眼定位到是哪一步变换写反了。

3. 核心代码实操:加载、投影、点云变换

3.1 环境配置与数据准备

环境方面的坑我先说结论,实测最稳的组合是Python 3.8到3.10加上nuscenes-devkit 1.1.10。官方包依赖pyquaternion、numpy等库,如果你环境里已经装了新版numpy,直接pip安装nuscenes-devkit可能会因为numpy API变动报一些奇怪的错误,建议用虚拟环境单独建一个,不要放在全局环境里折腾。

python -m venv nuscenes_env source nuscenes_env/bin/activate pip install nuscenes-devkit

数据准备方面,如果只是学习和调试代码,不需要把整个trainval下载下来。官方提供了v1.0-mini版本,大约4GB,包含了10个scene,足够把数据读取流程跑通。下载好解压后,目录结构要保持与dataroot设置一致。比如你解压到./data/nuscenes,里面有samples、sweeps、maps和v1.0-mini这几个子目录,初始化时dataroot填./data/nuscenes就行。

3.2 初始化NuScenes并遍历场景

初始化NuScenes对象时,它会一次性把所有JSON标注表加载进内存并建立token索引。这个过程需要一点时间,第一次跑看到卡顿不要慌,不是死锁,是在建表。

from nuscenes.nuscenes import NuScenes nusc = NuScenes( version='v1.0-mini', dataroot='./data/nuscenes', verbose=True ) # 查看第一个场景 my_scene = nusc.scene[0] print(my_scene['token'], my_scene['nbr_samples']) print(my_scene['first_sample_token']) # 拿第一个关键帧 sample = nusc.get('sample', my_scene['first_sample_token']) print(sample['timestamp']) print(list(sample['data'].keys()))

sample['data']是一个字典,key是传感器名称,包括CAM_FRONT、CAM_FRONT_LEFT、CAM_BACK_LEFT、LIDAR_TOP等,value是该传感器在sample_data表中的token。拿到token后再通过nusc.get('sample_data', token)就能拿到文件路径、时间戳、位姿token等完整信息。

这段代码虽然简单,但我建议认真看一遍print输出。很多初学者直接跳到模型复现,连sample和sample_data都分不清,后面写数据加载器时就会到处碰壁。

3.3 3D框投影到相机图像的手写实现

这一节是重点。3D框投影到相机图像,官方提供了get_sample_data接口,但我先给出手写实现,因为这里面每个矩阵的来龙去脉才是真正值钱的知识。

投影链路分三步:先把global坐标的3D框中心点变到ego坐标,再由ego变到相机sensor坐标,最后用相机内参投影到像素平面。

import numpy as np from pyquaternion import Quaternion # 找到前视相机的sample_data cam_data = nusc.get('sample_data', sample['data']['CAM_FRONT']) # 获取标定参数 calib = nusc.get('calibrated_sensor', cam_data['calibrated_sensor_token']) ego = nusc.get('ego_pose', cam_data['ego_pose_token']) # sensor -> ego sensor_to_ego = np.eye(4) sensor_to_ego[:3, :3] = Quaternion(calib['rotation']).rotation_matrix sensor_to_ego[:3, 3] = np.array(calib['translation']) # ego -> global ego_to_global = np.eye(4) ego_to_global[:3, :3] = Quaternion(ego['rotation']).rotation_matrix ego_to_global[:3, 3] = np.array(ego['translation']) # 反变换:global -> ego -> sensor global_to_ego = np.linalg.inv(ego_to_global) ego_to_sensor = np.linalg.inv(sensor_to_ego) global_to_sensor = ego_to_sensor @ global_to_ego # 取一个3D标注框(box.center是global坐标) box = nusc.get('sample_annotation', sample['data']['CAM_FRONT'])

等等,这里有个细节要说明一下,sample_annotation的token不是从sample['data']直接拿的,正确做法是获取该sample下的所有标注框再取第一个。修正代码如下:

ann_tokens = sample['anns'] box = nusc.get('sample_annotation', ann_tokens[0]) # box.center 是 global 坐标系的中心点 center_global = np.array(box['translation']) center_sensor = global_to_sensor[:3, :3] @ center_global + global_to_sensor[:3, 3] # 相机内参 intrinsic = np.array(calib['camera_intrinsic']) p = intrinsic @ center_sensor p = p / p[2] print('像素坐标:', p[:2])

代码跑通后,可以把所有标注框的中心点投影到图上,用matplotlib画出来验证。如果所有点都落在相机视野内且位置大致合理,说明变换链正确;如果发现点严重偏移甚至跑到图像外面,九成是旋转矩阵方向写反或者平移向量没加。

3.4 激光雷达点云读取与全局坐标变换

点云文件的读取用官方提供的LidarPointCloud类最省事。激光雷达点云文件里存的是sensor坐标系下的坐标,要变到global坐标系需要经过sensor → ego → global这样一条正变换链路。

from nuscenes.utils.data_classes import LidarPointCloud import os lidar_data = nusc.get('sample_data', sample['data']['LIDAR_TOP']) pc = LidarPointCloud.from_file(os.path.join(nusc.dataroot, lidar_data['filename'])) # 点云形状为 (4, N),前三行是xyz,第四行是强度 print(pc.points.shape) # 激光雷达的标定参数 calib_lidar = nusc.get('calibrated_sensor', lidar_data['calibrated_sensor_token']) ego_lidar = nusc.get('ego_pose', lidar_data['ego_pose_token']) lidar_to_ego = np.eye(4) lidar_to_ego[:3, :3] = Quaternion(calib_lidar['rotation']).rotation_matrix lidar_to_ego[:3, 3] = np.array(calib_lidar['translation']) ego_to_global = np.eye(4) ego_to_global[:3, :3] = Quaternion(ego_lidar['rotation']).rotation_matrix ego_to_global[:3, 3] = np.array(ego_lidar['translation']) # sensor -> ego -> global lidar_to_global = ego_to_global @ lidar_to_ego points_global = lidar_to_global[:3, :3] @ pc.points[:3, :] + lidar_to_global[:3, 3:4]

把点云变换到global坐标系后,再配合全局地图信息,可以验证点云是否与道路结构对齐。这个验证我在项目里做过很多次,结果基本一致,说明这套变换逻辑是可靠的。

如果你只是想快速把点云画出来看个大概,也可以直接用pc.points的前三行画3D散点图,不需要任何变换。但如果你想把雷达点云和相机图像做融合,那就必须做坐标系变换,不能偷懒。

4. 常见问题与排查技巧实录

4.1 安装和版本兼容问题

nuscenes-devkit的版本兼容性是个实实在在的坑。我用Python 3.11装最新版时,遇到过numpy.ndarray没有float属性这类报错,原因是numpy把np.float、np.int这类别名删掉了,而旧版devkit代码里还在用。后来我固定用Python 3.9加nuscenes-devkit 1.1.10,问题就消失了。

如果你在import阶段遇到pyquaternion报错,直接重装pyquaternion。这个包本身比较轻量,一般重装后能解决。另外不要在Windows系统上直接解压数据包到含中文的路径,nuscenes的代码内部对路径做了字符串拼接,中文路径偶尔会出现编码报错,把数据放在纯英文路径下是最省心的做法。

4.2 坐标变换输出异常怎么查

投影结果不对时,我建议按这个顺序排查。第一步检查ego_pose和calibrated_sensor是否取错,比如拿激光雷达的标定去投影相机数据;第二步检查旋转矩阵是否转置,Quaternion的rotation_matrix得到的矩阵是从sensor系到ego系的旋转,如果你把矩阵用反了,结果自然不对;第三步检查平移向量要不要加,很多人在做反变换时只做了旋转逆变换,忘了平移也需要变换,这个细节最容易漏。

另外一个有效的验证方式是拿官方get_sample_data的结果和手写结果对比。如果官方接口输出正确,而手写代码输出不对,就把中间每一步的矩阵打印出来,逐个对比。我遇到过好几次手滑把矩阵乘法的顺序写反,最后都是靠这种对比定位出来的。

4.3 数据加载慢与内存占用

nuScenes数据集如果不加节制地读取,内存会涨得很快。一个关键帧的点云文件大小还好,但如果你把所有sweeps的点云都加载进内存,几个scene下来就是几十GB,很容易把电脑拖垮。

我的经验是两个原则:按需加载,用完释放。在训练循环里,每次只读取当前sample需要的传感器数据,处理完就扔掉;不要用list把所有场景的数据先预加载一遍。另外,get_sample_data接口有一个参数use_flat_vehicle_coordinates,默认是True,意思是返回的box坐标已经投影到以自车为原点的平面坐标系。如果你要的是global坐标,记得把这个参数设为False,否则后续做全局变换会对不上。

4.4 标注文件JSON的实操经验

最后说一个不太起眼但很实用的经验:nuScenes的标注JSON文件在运行时是可以被修改的。如果你只想做数据子集采样,比如只保留车辆和行人两类,不需要重新造数据集,直接在dataroot下新建一个version文件夹,复制一份并缩小category表的字段就行。

我在项目里还遇到过一个和JSON解析有关的问题:标注文件里的category_name是带层级结构的,比如vehicle.car、vehicle.truck,在写分类映射时不能直接比对字符串是否相等,要用startswith或者split('.')[-1]来取叶节点名称,否则新版本数据集的类别层级一变,你的代码就默默失效了。

nuScenes这套数据链路里,最值钱的不是那几个调用接口,而是理解每一张表、每一个坐标系之间的映射关系。我个人的体会是,花一个下午把3D框投影到图像这个流程手写一遍,比看十遍文档都有用。你可以在跑通之后试着做一个可视化脚本,把相机图像、投影框、点云投影结果叠在一张图上,看到框稳稳贴在车身上、点云轮廓和图像边缘对齐的那一刻,整个数据集的逻辑基本就通透了大半。后续做BEV感知、多模态融合,都是在吃透这一层之后水到渠成的事。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 20:26:25

英语流利说秋招笔试题解析:从音素分割到高并发语音评测架构

从拿到英语流利说2019秋招技术类笔试题的那一刻起,我的第一反应是:这公司是真把业务揉进笔试里了。市面上大多数技术笔试都是教科书味的算法题,一套模板走天下,但流利说的卷子明显不同,字符串编辑距离、语音评测链路、…

作者头像 李华
网站建设 2026/9/1 20:22:59

美团校招测试简答题复盘:从测试思维到自动化框架的备考指南

去年秋招我在准备软件测试岗位时,把网上流传的“美团2023校招测试-简答题(第1/2批)”翻来覆去看了好几遍。第一眼的感觉是:这些题比算法题友好多了,至少能看懂题目在问什么。但真正动手写答案的时候才发现,简答题才是筛人的重头戏…

作者头像 李华
网站建设 2026/9/1 20:20:09

iHandy 2019校招技术笔试全解析:考点、答题思路与备考策略

每年秋招季,各类工具类App公司的技术笔试题总会被翻出来反复研究,iHandy的题就是其中一个绕不开的样本。这家公司做移动工具类产品出身,用户量级大、产品线多,所以笔试题目并不是单纯的“刷题筛人”,而是既考基本功&am…

作者头像 李华
网站建设 2026/9/1 20:19:35

用友Java笔试真题解析:从String到JVM与Spring核心考点

1. 这套题背后的出题逻辑:用友秋招Java笔试到底在考什么前两天整理硬盘,翻出了自己当年秋招时存的一份用友2018秋招Java笔试题,第六套。重看一遍感触挺深——用友这类传统软件大厂的笔试风格,和互联网大厂的出题思路确实不太一样。…

作者头像 李华
网站建设 2026/9/1 20:18:29

MySQL按中文排序:ORDER BY遇到中文乱序怎么办?5种方案

做后台开发的同学应该都碰到过这个场景:页面上有个下拉列表或者表格,需要按中文姓名、城市名排序显示,结果一查出来,张三排到李四前面还是后面完全看运气,搞得产品经理天天追着你问"这个排序怎么是乱的"。My…

作者头像 李华
网站建设 2026/9/1 20:11:41

Sentinel实战:微服务限流、熔断与降级的核心原理与落地

先说结论:Sentinel 是面向微服务、分布式系统的流量治理组件,核心就三件事:限流、熔断降级、系统保护。微服务里真正让人头疼的不是功能开发,而是流量一上来、下游一慢、某个接口一抖动,整个链路跟着挂。很多人把“限流…

作者头像 李华