简介:这份资源面向自动驾驶感知方向的研究者与Python开发者,聚焦nuScenes多模态数据集在Python环境下的解析与可视化实践,帮助读者打通从数据读取、传感器同步到结果呈现的完整链路。压缩包共8个文件,以3个py脚本为核心,辅以3个xml配置、1个pyc缓存与1个iml工程文件,整体约14KB,体量轻便,便于快速导入IDE运行与二次修改。代码围绕nuscenes-devkit展开,涉及样本数据访问、LiDAR点云分割、摄像头图像处理,以及检测与追踪评估模块的调用,并可能包含点云与图像的滤波降噪、子集划分和数据增强等预处理环节。可视化部分结合matplotlib、OpenCV及三维库展示相机视图与点云空间信息,帮助直观理解多模态感知数据。目前已有4615人学习下载,适合希望熟悉nuScenes数据结构、掌握自动驾驶数据处理与可视化方法的读者参考实践。
1. 拿到 NuScenesAnalysis.zip 之后:从解压到跑通第一条统计命令
很多人第一次接触 NuScenesAnalysis.zip 这类压缩包,是在做自动驾驶感知或轨迹预测的数据预处理阶段。它不是一个能双击运行的软件,而是一套围绕 nuScenes 数据集做统计、可视化、格式转换的脚本集合。你拿到的可能是同事交接的目录,也可能是从某个内部共享盘下载的归档。核心诉求很明确:把 nuScenes 的原始标注和传感器数据,变成训练框架能直接吃的中间格式,同时搞清楚数据分布长什么样。适合已经跑通过 nuScenes devkit、但被官方 API 的复杂嵌套劝退的人。我见过太多人卡在“数据加载器写完了,但不知道类别不平衡到什么程度”这一步,这个包就是解决这类问题的。
2. 先搞懂 nuScenes 的数据组织:为什么不能直接遍历文件夹
2.1 从 metadata 到 sample_data 的层级关系
nuScenes 的目录结构不是按“一帧一个文件夹”组织的。根目录下有v1.0-trainval、v1.0-test等版本文件夹,里面是若干 JSON 表文件:scene.json、sample.json、sample_data.json、ego_pose.json、calibrated_sensor.json、sensor.json、category.json、attribute.json、visibility.json、instance.json、annotation.json、log.json、map.json。这些表通过 token 字段互相引用,形成一张关系图。一个sample代表一个关键帧,它关联 6 个相机的sample_data和 1 个激光雷达的sample_data。sample_data里存的是文件路径、时间戳、标定 token。annotation挂在sample上,记录 3D 框、类别、属性、可见度。
直接遍历samples文件夹会得到一堆.jpg和.pcd,但不知道哪张图属于哪个场景、哪个时刻、哪个 ego pose。所以任何分析脚本的第一步,都是加载这些 JSON 表并建立索引。常见做法是用nuscenes-devkit的NuScenes类,但那个类初始化慢、内存占用大,做批量统计时不够轻量。NuScenesAnalysis.zip 里的脚本通常绕开 devkit,直接用json模块读表,再用pandas做 join。
2.2 用 pandas 重建一张可查询的宽表
下面这段代码把sample、sample_data、annotation、category四张表拼成一张扁平表,方便后续按类别、按场景、按时间做聚合。假设你已经把v1.0-trainval解压到./data/nuscenes/下。
import json import pandas as pd from pathlib import Path root = Path("./data/nuscenes/v1.0-trainval") def load_table(name): with open(root / f"{name}.json", "r") as f: return pd.DataFrame(json.load(f)) # 加载核心表 sample = load_table("sample") sample_data = load_table("sample_data") annotation = load_table("annotation") category = load_table("category") instance = load_table("instance") # sample 与 sample_data 通过 sample_token 关联 # 一个 sample 对应多个 sample_data(6 相机 + 1 激光雷达 + 若干雷达) sd = sample_data.merge( sample[["token", "scene_token", "timestamp"]], left_on="sample_token", right_on="token", suffixes=("_sd", "_sample") ) # annotation 挂在 sample 上,同时关联 instance 和 category ann = annotation.merge( instance[["token", "category_token"]], left_on="instance_token", right_on="token", suffixes=("_ann", "_inst") ).merge( category[["token", "name"]], left_on="category_token", right_on="token", suffixes=("_ann", "_cat") ) # 统计每个类别的标注框数量 cat_counts = ann.groupby("name_ann").size().sort_values(ascending=False) print(cat_counts.head(20))逻辑说明:sample_data里既有相机也有激光雷达,用sample_token关联到sample后,可以按filename后缀过滤出相机或激光雷达。annotation表本身没有类别名,需要先通过instance_token找到instance,再通过category_token找到category。参数方面,suffixes用来避免同名列冲突,groupby后的size()统计的是每个类别的框数,不是实例数——同一个实例在不同帧会出现多次,如果要统计唯一实例,应该对instance_token去重。
提示:nuScenes 的 JSON 表里 token 是 32 位十六进制字符串,pandas 默认按 object 类型处理,做 merge 时速度尚可。如果内存吃紧,可以把 token 列转成 category 类型,能省一半内存。
3. 用 NuScenesAnalysis 做类别分布与场景筛选:三个必调参数
3.1 类别不平衡到底有多严重:先看长尾
跑完上面的统计,你会看到car、pedestrian、barrier这类常见类别占了绝大多数,而bicycle、motorcycle、trailer等长尾类别可能只有几百个框。如果直接拿全量数据训练,模型会严重偏向头部类别。NuScenesAnalysis 里通常有一个filter_by_category函数,用来按类别筛选样本。但筛选之前,得先知道每个类别的实例数和框数。
# 统计每个类别的唯一实例数(去重 instance_token) inst_counts = ann.drop_duplicates("instance_token").groupby("name_ann").size() # 统计每个类别的总框数 box_counts = ann.groupby("name_ann").size() # 合并成一张对比表 dist = pd.DataFrame({"instances": inst_counts, "boxes": box_counts}).fillna(0) dist["boxes_per_instance"] = dist["boxes"] / dist["instances"] print(dist.sort_values("boxes", ascending=False))boxes_per_instance这个指标很关键。如果某个类别的值接近 1,说明这个类别的物体在场景里几乎不动,或者只出现一帧;如果值很大,说明物体被持续跟踪了很多帧。对于轨迹预测任务,boxes_per_instance太小的类别要谨慎使用,因为无法构成有效的轨迹序列。
3.2 场景筛选:按地点、时间、天气过滤
nuScenes 的scene表里有name、description字段,log表里有location字段。常见需求是只取某个城市或某个时间段的数据。NuScenesAnalysis 里一般会提供一个select_scenes函数,参数包括locations、time_of_day、weather。但原始 JSON 里没有结构化的天气字段,需要从description里做字符串匹配。
scene = load_table("scene") log = load_table("log") # 把 scene 和 log 关联,拿到 location scene_log = scene.merge(log[["token", "location"]], left_on="log_token", right_on="token") # 按 location 筛选,比如只要 singapore 的数据 sg_scenes = scene_log[scene_log["location"] == "singapore"] print(f"Singapore scenes: {len(sg_scenes)}") # 按 description 里的关键词筛选夜间场景 night_scenes = scene_log[scene_log["description"].str.contains("night", case=False, na=False)] print(f"Night scenes: {len(night_scenes)}")参数说明:locations的取值通常是boston、singapore等,具体取决于数据集版本。time_of_day和weather没有独立字段,只能从description里做关键词匹配,比如night、rain、day。这种匹配不精确,但做粗筛够用。筛选后的scene_token列表可以传给后续的sample过滤逻辑。
3.3 采样频率与关键帧对齐
nuScenes 的sample是 2Hz 关键帧,sample_data里还有非关键帧。做轨迹预测时,通常只用关键帧,因为标注只挂在关键帧上。但做原始传感器融合时,可能需要用非关键帧来补足帧率。NuScenesAnalysis 里一般会有一个align_frames函数,把相机、激光雷达、雷达的时间戳对齐到同一个sample上。
# 只看关键帧的 sample_data key_sd = sd[sd["is_key_frame"] == True] # 按 sample_token 分组,统计每个 sample 有多少个传感器数据 sensor_per_sample = key_sd.groupby("sample_token").size() print(sensor_per_sample.describe()) # 正常一个 sample 应该有 6 相机 + 1 激光雷达 + 5 雷达 = 12 个左右 # 如果某个 sample 少于 10 个,说明数据缺失,需要排查is_key_frame是sample_data里的布尔字段。如果某个关键帧的传感器数量明显偏少,可能是数据下载不完整或解压出错。这时候不要急着跑训练,先把缺失的 sample_token 打印出来,对照文件系统检查对应的.jpg或.pcd是否存在。
4. 避坑与排查:NuScenesAnalysis 跑不通的五个常见原因
4.1 现象:JSON 加载报 UnicodeDecodeError
原因:nuScenes 的 JSON 表文件默认是 UTF-8,但某些解压工具在 Windows 上会按 GBK 重新编码,导致中文字符或特殊符号解码失败。解决:用open(path, "r", encoding="utf-8")显式指定编码,如果仍然报错,用errors="ignore"跳过非法字节,但这样会丢数据,最好重新解压。
4.2 现象:merge 后行数暴涨
原因:sample_data里同一个sample_token对应多条记录,如果直接和annotationmerge,会产生笛卡尔积。解决:先按sample_token聚合sample_data,或者明确 join 的粒度。比如统计每个 sample 的标注框数时,应该用annotation.groupby("sample_token").size(),而不是把sample_data拉进来。
4.3 现象:类别名对不上,出现vehicle.xxx和human.pedestrian混用
原因:nuScenes 的类别名是层级结构,比如vehicle.car、vehicle.bicycle、human.pedestrian.adult。有些脚本只取第一级,有些取全名。解决:统一用category.json里的name字段,不要自己截取。如果要做粗分类,用str.split(".")[0]取顶级类别,但要清楚这会丢失细粒度信息。
4.4 现象:内存溢出,pandas 吃满 32G
原因:sample_data表有几十万行,annotation表有上百万行,直接全量加载再 merge 会爆内存。解决:分块读取,或者只加载需要的列。比如统计类别分布时,只需要annotation的instance_token和category_token,以及instance的category_token,不需要加载sample_data。用usecols参数限制列。
4.5 现象:时间戳对不上,ego pose 插值失败
原因:ego_pose的时间戳是微秒级,sample_data的时间戳也是微秒级,但两者不是严格一一对应。做轨迹插值时,如果直接按时间戳 merge,会丢帧。解决:用pd.merge_asof做最近邻匹配,设置tolerance参数,比如 50000 微秒(50ms)。超过这个阈值的帧标记为无效。
5. 进阶:把统计结果落成可视化与可复现的配置文件
5.1 用 matplotlib 画类别分布直方图
统计完类别分布后,直接打印数字不够直观。下面这段代码把cat_counts画成水平条形图,方便快速判断长尾程度。
import matplotlib.pyplot as plt top_n = 15 plot_data = cat_counts.head(top_n).sort_values() fig, ax = plt.subplots(figsize=(8, 6)) plot_data.plot(kind="barh", ax=ax, color="#4c72b0") ax.set_xlabel("Number of boxes") ax.set_ylabel("Category") ax.set_title("Top categories in nuScenes trainval") plt.tight_layout() plt.savefig("category_dist.png", dpi=150)参数说明:top_n控制显示前多少个类别,sort_values()让条形图从下到上递增,视觉上更符合阅读习惯。dpi设 150 足够看清标签,再高文件会很大。如果类别名太长,可以用ax.set_yticklabels做缩写。
5.2 把筛选条件写成 YAML,避免脚本里硬编码
每次改筛选条件都去改 Python 代码很麻烦,也容易出错。常见做法是把locations、time_of_day、categories、min_boxes这些参数写进一个 YAML 文件,脚本启动时读取。
# config/filter.yaml locations: - singapore time_of_day: night categories: - vehicle.car - human.pedestrian.adult min_boxes_per_scene: 5 output_dir: ./output/night_sgimport yaml with open("config/filter.yaml", "r") as f: cfg = yaml.safe_load(f) # 后续用 cfg["locations"] 等变量做筛选 print(cfg["categories"])这样做的好处是,同一套脚本可以跑不同的数据子集,结果目录也按配置命名,方便对比。min_boxes_per_scene用来过滤掉标注太少的场景,避免噪声样本进入训练集。
5.3 验证筛选结果:用 scene_token 反查原始文件
筛选完成后,不要直接开始训练。先抽几个scene_token,反查对应的sample和sample_data,确认文件路径存在、时间戳连续、标注框数量合理。我一般会写一个validate_subset.py,随机抽 10 个场景,打印每个场景的帧数、类别分布、首尾时间戳差值。如果某个场景的帧数明显少于预期,或者时间戳跳跃超过 1 秒,就要检查是不是筛选逻辑把关键帧漏掉了。
import random sample_subset = sample[sample["scene_token"].isin(selected_scene_tokens)] for scene_token in random.sample(list(selected_scene_tokens), 10): frames = sample_subset[sample_subset["scene_token"] == scene_token] ts = frames["timestamp"].sort_values() gaps = ts.diff().dropna() print(f"Scene {scene_token[:8]}: {len(frames)} frames, " f"max gap {gaps.max()/1e6:.2f}s, min gap {gaps.min()/1e6:.2f}s")timestamp单位是微秒,除以1e6转成秒。正常关键帧间隔是 0.5 秒,如果max gap超过 1 秒,说明中间有丢帧。这时候要么补数据,要么把这个场景从训练集里剔除。这个习惯帮我省过很多次“训练 loss 不降”的排查时间——数据有问题,模型再调参也是玄学。希望帮到你。
本文还有配套的精品资源,点击获取