☰
自动驾驶数据集可视化:点云、3D标注与坐标变换实践
2026/10/20 3:50:34 网站建设 项目流程

简介:面向自动驾驶研究和开发人员,这份 NuScenesAnalysis.zip 提供基于 Python 的 nuScenes 多模态感知数据解析与可视化示例工程。资源共 8 个文件,包含 3 个 Python 源码(主逻辑、辅助函数等)、3 个工程配置 XML、1 个编译后的 pyc 文件及 1 个模块描述 iml,压缩包仅 14KB,结构精简。已有 4615 人学习浏览,具备一定参考热度。通过运行和阅读代码,可掌握利用 nuscenes-devkit 读取激光雷达、摄像头等多传感器数据,完成点云分割、图像提取、检测与追踪结果评估,并结合 matplotlib、OpenCV 等工具实现二维与三维可视化的完整流程;工程中的 .idea 配置和辅助脚本也便于快速搭建开发环境,适合熟悉 Python 并希望快速上手 nuScenes 数据集的初中级开发者。

1. 项目定位解析:NuScenesAnalysis 是怎么把自动驾驶数据集变成可视化分析工作台的

拿到自动驾驶数据集,第一件事往往是懵的。nuScenes 原始包里除了几十 GB 的 bin 文件,就是一堆 json 和 yaml,传感器标定、位姿、标注信息散落在不同文件里,想看一眼某个场景长什么样,得自己拼代码。NuScenesAnalysis.zip 解决的问题就是把这个黑匣子拆开:它是一个基于 Python 生态的分析工具集,通过 nuScenes-devkit 读取数据集,把激光雷达点云、相机图像、3D 标注框和轨迹信息统一到一套可复现的流程里,最终输出可视化结果、统计报表和可二次开发的数据结构。适合需要快速理解 nuScenes 数据结构、验证模型输入输出、或者做数据预处理的算法工程师和研究者。

2. 技术栈与目录结构:为什么这套分析工具选择了 Python 生态

2.1 技术选型:nuScenes-devkit 之外,还要依赖哪些库

这个资源的核心依赖是 nuscenes-devkit,它是官方维护的 Python 工具包,封装了数据读取、坐标变换和标注渲染。分析工具在此基础上做了进一步封装,主要依赖以下库:

  • nuscenes-devkit:提供 NuScenes 类、LidarPointCloud、CameraPointCloud 等基础接口,负责从磁盘加载标注 json 和二进制点云;
  • numpy:所有点云和矩阵运算的基础,坐标变换、过滤、统计都靠它;
  • matplotlib:用于绘制 BEV 鸟瞰图、显示相机图像和标注框投影;
  • pyquaternion:处理四元数,用于传感器位姿和坐标旋转;
  • open3d(可选):如果启用了 3D 交互式可视化,会用 Open3D 渲染点云窗口。

这些依赖是逐层递进的:核心的数据访问靠 devkit,统计和可视化靠 numpy 和 matplotlib,如果需要交互式查看点云再启用 open3d。

2.2 资源目录结构:从解压到能跑,先看清每个文件夹的作用

解压 NuScenesAnalysis.zip 后,目录大致是这样组织的:

NuScenesAnalysis/ ├── analysis/ │ ├── load_scene.py # 数据加载器,封装场景和样本读取 │ ├── visualize.py # 可视化模块,BEV 渲染和传感器投影 │ ├── statistics.py # 统计模块,类别分布、场景帧数、标注尺寸 │ └── export.py # 数据导出,转换为 COCO 或通用 json 格式 ├── config/ │ └── config.yaml # 配置文件:数据路径、版本、可视化参数 ├── scripts/ │ └── run_analysis.py # 命令行入口 ├── requirements.txt └── README.md

分析模块是核心,四个文件各司其职。load_scene.py 负责所有数据读取,visualize.py 负责把数据画出来,statistics.py 负责统计指标,export.py 负责把数据转成其他格式。config.yaml 控制运行参数。这种分层结构的好处是:如果你想新增一个分析功能,只需要在 analysis 下加一个文件,然后在 run_analysis.py 里注册一个新命令,不需要动其他模块。

3. 核心模块拆解:一个场景样本从加载到可视化的完整链路

3.1 数据加载器:用 NuScenes 类解开场景与样本的索引关系

nuScenes 数据的组织方式是层级索引的:数据库里有 scene(场景)、sample(采样帧)、sample_data(传感器数据)、sample_annotation(标注框)四层结构。load_scene.py 的核心任务是给定一个场景编号,把这一层索引关系串起来。

from nuscenes.nuscenes import NuScenes import numpy as np class SceneLoader: def __init__(self, dataroot: str, version: str = "v1.0-mini"): # 初始化 NuScenes 数据库,verbose=False 关闭日志刷屏 self.nusc = NuScenes(version=version, dataroot=dataroot, verbose=False) def load_scene_samples(self, scene_idx: int): # 1. 拿到场景对象 scene = self.nusc.scene[scene_idx] # 2. 从场景第一个样本开始沿 next 指针遍历 sample_token = scene["first_sample_token"] samples = [] while sample_token != "": sample = self.nusc.get("sample", sample_token) samples.append(sample) sample_token = sample["next"] return scene, samples def load_lidar_points(self, sample_data_token: str): # 用 devkit 内置接口读取激光雷达二进制文件 lidar_path, boxes, _ = self.nusc.get_sample_data(sample_data_token) points = np.fromfile(lidar_path, dtype=np.float32).reshape(-1, 5) return points # [x, y, z, intensity, ring_index]

核心逻辑在 load_scene_samples 里。devkit 的 scene 对象用 first_sample_token 指向第一个 sample,每个 sample 又用 next 指针串起整个场景,所以用 while 循环遍历即可。get_sample_data 这个接口很重要,它内部已经处理了坐标变换,返回的 boxes 是经过位姿变换后位于全局坐标系下的 3D 标注框,和原始点云可以直接叠加。

参数说明:version 参数建议用 v1.0-mini 做调试,数据量小,加载速度快;正式分析时改成 v1.0-trainval。dataroot 指向 nuScenes 数据集解压后的根目录,里面要包含 maps、samples、sweeps 等子目录。这里有个容易被忽略的细节:get_sample_data 返回的 lidar_path 已经拼接了 dataroot,不需要手工拼路径。

3.2 3D 标注框可视化:把类别和尺寸画到点云上

可视化是理解数据最快的路径。visualize.py 里实现了两个层次的渲染:BEV 鸟瞰图和相机图投影。先看 BEV 渲染的实现。

import matplotlib.pyplot as plt def render_bev_with_boxes(self, sample_token: str) -> np.ndarray: sample = self.nusc.get("sample", sample_token) # 取激光雷达对应的 sample_data lidar_token = sample["data"]["LIDAR_TOP"] points = self.loader.load_lidar_points(lidar_token) # BEV 视图只保留 x 和 y 轴,过滤掉高度信息 bev_points = points[:, :2] # 简单去噪:去掉距离原点过远的点 mask = np.linalg.norm(bev_points, axis=1) < 50.0 bev_points = bev_points[mask] # 获取该帧所有 3D 标注框 _, boxes, _ = self.nusc.get_sample_data(lidar_token) fig, ax = plt.subplots(figsize=(10, 10)) # 用散点图绘制点云,s=0.1 控制点大小避免过密 ax.scatter(bev_points[:, 0], bev_points[:, 1], s=0.1, c="gray", alpha=0.6) for box in boxes: # 取框的中心点和偏航角,用矩形绘制 corners = box.bottom_corners() # 4x2 的矩阵,代表底面四个角 ax.plot(corners[0, [0,1,2,3,0]], corners[1, [0,1,2,3,0]], linewidth=2) ax.text(box.center[0], box.center[1], box.name, fontsize=8) ax.set_xlim(-50, 50) ax.set_ylim(-50, 50) plt.axis("off") return fig

这段代码的关键点是 bottom_corners 方法。box 对象内部存的是中心点、尺寸和四元数,bottom_corners 根据这些信息计算出底面四个角的坐标,直接 plot 出来就是带朝向的矩形框。加上 ax.text 在框中心标注类别名,可以直观看到每个目标的类别分布。如果你觉得散点图太密,可以用 ax.hexbin 代替,速度更快,视觉上更均匀。

3.3 传感器数据同步:相机图像和激光雷达的坐标对齐

nuScenes 一个 sample 包含多个传感器的数据,相机和激光雷达的时间戳是同步的,但坐标系不同。要把 3D 标注框投影到相机图像上,需要经过一系列坐标变换。devkit 提供了现成接口,但这个资源里把过程拆开做了,方便二次开发。

def project_boxes_to_camera(self, sample_token: str, cam_token: str): # 1. 获取相机的外参和内参 cam_data = self.nusc.get("sample_data", cam_token) cam_calib = self.nusc.get("calibrated_sensor", cam_data["calibrated_sensor_token"]) cam_intrinsic = cam_calib["camera_intrinsic"] # 2. 获取激光雷达相对于相机坐标系的位姿 lidar_data = self.nusc.get("sample_data", self.nusc.get("sample", sample_token)["data"]["LIDAR_TOP"]) lidar_calib = self.nusc.get("calibrated_sensor", lidar_data["calibrated_sensor_token"]) # 3. 获取 ego 车辆的全局位姿(两个传感器共享同一个 ego_pose) ego_pose = self.nusc.get("ego_pose", lidar_data["ego_pose_token"]) # 4. 用 devkit 的坐标变换工具完成转换 from nuscenes.utils.data_classes import Box # 构造一个完整变换:lidar -> ego -> global -> ego -> cam # 这里直接复用 devkit 的 get_sample_data 简化操作 _, boxes, _ = self.nusc.get_sample_data(lidar_data["token"]) # 旋转到相机坐标系 boxes = [box.rotate(Quaternion(cam_calib["rotation"]).inverse) for box in boxes] boxes = [box.translate(-np.array(cam_calib["translation"])) for box in boxes] # 投影到像素平面 corners = [box.corners() for box in boxes] pixel_corners = [cam_intrinsic @ corner for corner in corners] return pixel_corners

这段代码展示的是手工变换流程。核心思想是:先把 3D 框从全局坐标系变换到相机坐标系,再用相机内参矩阵做透视投影。手工拆开的好处是你可以在中间任意一步插入自定义处理,比如只保留正前方的目标。实际项目中我一般优先用 devkit 的 get_sample_data 直接拿投影结果,只有当需要批量处理时才手工写,因为每帧都调用 get_sample_data 会比较慢。

4. 运行指南:从零开始把 NuScenesAnalysis 跑起来

4.1 环境准备与依赖安装

假设你已经下载了 nuScenes 数据集(至少 v1.0-mini 版本),接下来安装 Python 依赖。推荐用虚拟环境隔离,避免污染系统 Python。

python -m venv nusc_env source nusc_env/bin/activate pip install -r requirements.txt

requirements.txt 里的核心依赖版本需要注意:nuscenes-devkit 目前对 numpy 的版本要求不太严格,但如果你同时装了 open3d,numpy 版本不能太新(open3d 0.18 以下版本对 numpy 2.x 兼容性不好)。我实际跑的时候用过 numpy 1.24 + open3d 0.17 的组合,稳定没出问题。

4.2 配置文件解析与修改

config.yaml 是运行的入口配置,默认内容如下:

data: dataroot: "/path/to/nuscenes" version: "v1.0-mini" analysis: scene_idx: 0 render_bev: true render_camera: true filter_categories: ["vehicle.car", "vehicle.truck", "pedestrian"] export: output_dir: "./output" format: "coco"

dataroot 改成你本机的数据集路径,version 保持不变。scene_idx 指定要分析的场景编号,从 0 开始。filter_categories 是一个关键参数,它决定统计结果和可视化只保留哪些类别,如果你想查看所有类别,把这个列表清空即可。format 参数支持 coco 和 json 两种导出格式。

4.3 三个典型运行命令

运行入口统一在 scripts/run_analysis.py,通过命令行参数区分工作模式。

# 1. 可视化一个场景的 BEV 鸟瞰图 python scripts/run_analysis.py --mode visualize --scene-idx 0 # 2. 统计整个数据集(或某个场景)的类别分布 python scripts/run_analysis.py --mode stats --scene-idx 0 # 3. 导出 COCO 格式的标注文件 python scripts/run_analysis.py --mode export --scene-idx 0 --format coco

mode 参数有三个值:visualize、stats、export,分别对应可视化和图表生成、统计报告输出、数据格式导出。每次运行完后,结果默认输出到 config.yaml 中配置的 output_dir 下。如果运行时提示找不到 dataset 路径,优先检查 dataroot 末尾有没有斜杠,以及 version 是否和数据集版本匹配——这两个是踩坑率最高的问题。

5. 常见问题排查:坐标偏移、内存溢出和标注缺失的现场记录

5.1 坐标偏移:点云和标注框整体错位,框跑到点云外面

现象:渲染 BEV 图时,点云分布正常,但标注框整体偏离点云中心,看起来像是两个坐标系叠加错了。

原因:最常见的是 version 参数选错。nuScenes 官方的 v1.0-mini 和 v1.0-trainval 在标注坐标定义上不完全一致,如果你用 v1.0-trainval 的数据加载,却在代码里传了 v1.0-mini 的版本字符串,devkit 内部会读取错误版本的 json 索引文件,坐标自然对不上。另一个原因可能是你手动做了坐标变换,但忘记了转换顺序。

解决:首先确认 NuScenes 对象初始化时的 version 参数与数据集目录下实际存在的 version 文件夹一致。然后检查可视化代码里是否在 get_sample_data 返回的基础上又做了一次旋转或平移,如果有,注释掉试一次。我从那以后每次初始化 NuScenes 都会打印 self.nusc.version 看一眼,排查这类问题效率高很多。

5.2 内存溢出:加载整个场景直接卡死,进程被系统杀掉

现象:运行 stats 模式时,程序运行一段时间后内存持续攀升,最后 OOM。

原因:stats 模式需要遍历场景的所有帧,每帧都需要读取点云和标注。如果你在循环里把每一帧的点云都 accumulate 到一个 list 里再去统计,内存自然爆掉,因为一个场景几十帧,每帧 3 万个点,乘以 float32 的 5 个通道,轻松突破 GB 级别。

解决:改成流式处理,每一帧统计完后立即释放点云变量。用 del 显式删除,或者把统计逻辑封装在函数内部,让局部变量随函数退出自动回收。核心代码逻辑是:每帧只保留统计结果(比如框数量、点云点数),不保留原始点云。另外,如果你开了 open3d 可视化窗口,也要注意关闭窗口后再进入下一帧,否则显存和内存一起涨。

5.3 标注缺失:过滤类别后统计结果为空

现象:配置了 filter_categories 后,输出报告里表格是空的,或者是 0。

原因:过滤条件里的类别字符串写错了。nuScenes 的类别是层级结构,完整的类别名是 vehicle.car、vehicle.truck、human.pedestrian 这种风格,不是简单的 car 或 pedestrian。如果你只写 car,匹配不上,devkit 不会报错,只是匹配结果为 0。

解决:先运行一次不设过滤条件的统计,把完整类别列表打印出来,看清楚层级。然后从输出列表里复制类别名到配置文件中。我在做数据清洗时一般是先打印 categories,再决定过滤条件,很少手写类别字符串。

6. 二次开发:把 NuScenesAnalysis 改造成自定义数据集的通用分析工具

6.1 扩展新的可视化模式

默认的可视化只支持 BEV 鸟瞰图和相机投影,如果你想增加一个侧视图(XZ 平面投影),只需要在 visualize.py 里新增一个函数,然后在 run_analysis.py 里增加一个 mode 分支。侧视图的代码基本是复制 render_bev_with_boxes,只是把 x 轴换成 z 轴,实际改动量很小。

def render_side_view(self, sample_token: str): sample = self.nusc.get("sample", sample_token) lidar_token = sample["data"]["LIDAR_TOP"] points = self.loader.load_lidar_points(lidar_token) # 侧视图保留 x 和 z 轴 view_points = points[:, [0, 2]] mask = np.abs(view_points[:, 0]) < 50.0 view_points = view_points[mask] plt.scatter(view_points[:, 0], view_points[:, 1], s=0.1, c="gray", alpha=0.6) plt.xlabel("x") plt.ylabel("z") plt.axis("equal")

6.2 导出自定义格式:从 COCO 扩展到 YOLO 格式

export.py 目前支持 COCO 格式,但如果你要把标注数据喂给 YOLO 系列检测器,需要转换成 YOLO 的 txt 格式。改动点在 export.py 里加一个 to_yolo 函数,核心逻辑是把 3D 框投影到相机图像后,归一化中心坐标和宽高,写入 txt 文件。这里有一个关键参数:YOLO 格式的类别 id 需要和你的类别列表一一对应,建议导出一个类别映射 json 文件,便于之后训练时对齐。

6.3 接入新的数据源:把加载器抽象出通用接口

load_scene.py 目前的实现绑定了 nuScenes 数据结构。如果将来想分析自己的数据集,需要把 SceneLoader 抽象成一个接口,定义 get_scene_info、get_frame_points、get_annotations 三个方法,然后为每个数据集写一个实现类。这样上层可视化和统计模块完全不用改,只替换加载器。我在做多数据集对比实验时,就是通过这种方式统一了 nuScenes 和自采数据的分析流程,省了不少重复工作。

整个项目拆下来,最大的收获是理解了 nuScenes 数据组织的层级结构,以及 devkit 各接口背后的坐标变换逻辑。从那以后我再分析新的传感器数据集,第一件事就是先用这个工具跑一遍数据可视化,确认坐标系和标注格式没问题再动模型训练。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询