ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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对应一次完整的“同时刻采集”频率是2Hzsweeps则是两个关键帧之间其他时刻的数据主要用于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( versionv1.0-mini, dataroot./data/nuscenes, verboseTrue ) # 查看第一个场景 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感知、多模态融合都是在吃透这一层之后水到渠成的事。本文还有配套的精品资源点击获取
返回列表