☰
nuScenes lidarseg与panoptic实战:数据格式、加载与评估指南
2026/9/29 9:40:16 网站建设 项目流程

上一篇文章我们把 nuScenes 数据集的整体目录、3D 检测相关的 sample、sweep、标注表都过了一遍。今天这篇是续集,专门解决 lidarseg 和 panoptic 这两块硬骨头。说实话,这两个任务在 nuScenes 里的定位非常有意思:它们都建立在原始 LIDAR 点云上,但一个是纯语义分割,一个是带实例的全景分割,很多新手第一次接触时容易把两者搞混。这篇我不打算只讲概念,而是直接带着你走一遍从数据集下载、目录拆解、标签格式解析到代码加载、可视化、评估指标的完整链路,保证你看完能直接上手跑通自己的流程。

1. 项目概述:这两个任务到底在做什么

1.1 从纯语义分割到全景分割

nuScenes lidarseg 和 panoptic 并不像 3D 检测框那样用长方体标注物体,而是在每个激光雷达点上直接给标签。lidarseg 做的是逐点语义分类,比如这个点属于 car、pedestrian、sidewalk 还是 vegetation,每个点只有一个语义类别 ID。panoptic 则在语义分割基础上额外引入实例信息,要求同一类别的不同物体区分开,比如两辆并排停着的车,语义标签都是 car,但实例 ID 必须不同。

这套数据和 3D 检测相比,最直观的差异在于标注粒度和任务定义。检测框解决的是"物体在哪儿、是什么、多大"的问题,而 lidarseg 解决的是"每个点属于哪个类别"的问题,panoptic 进一步解决"每个点属于哪个物体的哪个类别"的问题。从产业应用来看,lidarseg 和 panoptic 的高质量标注通常用于训练感知模型、生成 occupancy ground truth、离线高精地图要素提取,以及作为多传感器融合的监督信号。

1.2 为什么值得单独写一篇

我在社区里看到不少人卡在同一个地方:下载了 nuScenes 完整数据集,结果发现 lidarseg 文件夹里的标注文件打不开,或者按官方 demo 跑渲染时报错,甚至有人把 panoptic 的 uint16 标签直接用 uint8 读,导致实例信息全丢。这些坑大多不是算法问题,而是对数据格式理解不够透。

另一个原因是 nuScenes 官方对 lidarseg 和 panoptic 的支持方式比较特殊,两者共用一套存储目录和加载入口,但编码方式不同、类别体系不同、评估指标也不同。如果你用处理 lidarseg 的方式去读 panoptic,或者反过来,数据本身就废了。这篇就把这些细节全部摊开讲清楚。

2. nuScenes 数据集下载与数据目录结构

2.1 下载前的注册与授权步骤

先解决最基础的下载问题。nuScenes 数据集不是直接丢给你一个网盘链接,需要去官网注册账号,填写机构和用途信息,然后提交申请。审核通过后,你会在下载页面看到多个数据包,包括完整训练集、验证集、测试集、地图包、CAN bus 扩展包,以及 lidarseg 和 panoptic 标注包。

这里有个特别容易踩的坑:很多人在下载页面只勾选了核心 sensor data 和 3D 检测标注,忽略了 lidarseg 和 panoptic 这两个独立压缩包。等你代码跑起来发现nusc.lidarseg是空列表,或者 lidarseg 目录根本不存在,再回去补下载就很浪费时间。我的建议是申请时直接把所有能勾的包都勾上,尤其是一并下载 map、CAN bus、lidarseg 和 panoptic,免得后续扩展实验时又要等下载。下载完成后,用sha256sum之类的工具校验一下压缩包完整性,官方页面提供了校验值,别省这一步,我曾经遇到过一个标称 10GB 的包下载损坏,解压时报错,浪费了半天时间排查。

2.2 解压后的标准目录结构

下载完成后,把所有压缩包解压到同一个根目录,标准的 nuScenes 目录结构是这样的:

nuScenes/ ├── maps/ # 地图栅格与矢量文件 ├── samples/ # keyframe 数据(约每 2 秒一帧) │ ├── CAM_FRONT/ │ ├── CAM_BACK/ │ ├── LIDAR_TOP/ │ └── RADAR_FRONT/ ├── sweeps/ # 非 keyframe 的中间帧 ├── lidarseg/ # lidarseg 与 panoptic 标注目录 │ └── v1.0-trainval/ ├── v1.0-trainval/ # 核心 JSON 元数据表 │ ├── sample.json │ ├── sample_data.json │ ├── sample_annotation.json │ ├── category.json │ ├── attribute.json │ ├── lidarseg.json # lidarseg 标注索引 │ ├── panoptic.json # panoptic 标注索引 │ └── ... └── (其他版本目录,如 v1.0-mini)

注意lidarseg/目录下面的二级目录名会和版本目录对应,比如v1.0-trainval对应完整数据,v1.0-mini对应迷你版。不同版本的数据不要混着用,否则 JSON 索引和 bin 文件对不上。

2.3 lidarseg 和 panoptic 文件到底放哪儿

这是很多人第一次接触时的疑惑点:nusc.lidarseg和nusc.panoptic这两个属性,对应的数据文件都在lidarseg/v1.0-trainval/目录下,文件名是.bin格式,命名规则是sample_data_token.bin。每个 bin 文件对应一帧 LIDAR_TOP 的 keyframe 数据,文件内部保存的是该帧所有点的标签数组。

这些 bin 文件没有额外头信息,就是一堆打包的整数。lidarseg 的标签是uint8类型,每个点占 1 字节;panoptic 的标签是uint16类型,每个点占 2 字节。同一点的坐标所在的点云文件来自samples/LIDAR_TOP/xxx.pcd,两者按点顺序一一对应。官方 JSON 索引存放在v1.0-trainval/lidarseg.json和v1.0-trainval/panoptic.json中,里面记录了每个 sample_data token 对应的 bin 文件路径。

3. lidarseg 数据格式与加载实操

3.1 语义标签的存储原理

lidarseg 的 bin 文件可以理解为一张一维整数表,数组长度等于对应 LIDAR_TOP 点云中的点数。每个整数的含义是语义类别 ID。官方定义的类别体系包括 background 和 foreground 两大类,标准类别从 1 开始,0 被保留给 ignore 区域,评估时类别 0 不参与计算。

用代码读取很直接:

import numpy as np import os from nuscenes import NuScenes nusc = NuScenes(version='v1.0-trainval', dataroot='/data/nuscenes', verbose=True) sample = nusc.sample[0] lidar_token = sample['data']['LIDAR_TOP'] sample_data = nusc.get('sample_data', lidar_token) # 方法一:通过索引表手动定位 bin 文件 lidarseg_by_token = {rec['sample_data_token']: rec for rec in nusc.lidarseg} lidar_rec = lidarseg_by_token[lidar_token] lidar_bin_path = os.path.join(nusc.dataroot, lidar_rec['filename']) semantic_labels = np.fromfile(lidar_bin_path, dtype=np.uint8) print(semantic_labels.shape) # 形状应该与点云点数一致 print(np.unique(semantic_labels)) # 打印出现的类别

如果你的 devkit 是较新的版本,也可以尝试nusc.get_lidarseg(lidar_token)直接拿标注结果,底层逻辑和上面手动解析是一样的,只不过官方封装了一层取数逻辑。两种方式我在实际项目中都用过,手动解析更可控,适合需要同时处理原始点云和标签的场景。

3.2 类别体系与 ignore 处理

nuScenes lidarseg 的完整类别清单较长,核心是围绕自动驾驶常见物体和道路环境定义。车辆相关包括 car、bus、truck、trailer、construction_vehicle、bicycle、motorcycle,行人相关包括 pedestrian 和 personal_mobility,道路相关包括 driveable_surface、sidewalk、terrain、manmade、vegetation、other_flat,以及 barrier、traffic_cone 等静态物。

在训练自己的模型时,有两个和类别相关的细节容易被忽视:

  • 一定要用nusc.lidarseg中的category_name和category_id映射来构建自己的类别索引,不要手工写死数字。不同版本的数据集类别编号可能调整,写死很容易出 bug。
  • 评估时必须排除类别 0(ignore)。很多新手直接对所有类别算 mIoU,结果 ignore 点占比一高,整个指标就失真。官方评估脚本会自动排除 ignore,你自己实现评估时也要记得做 mask。

3.3 点云逐点可视化

拿到语义标签后,最常见的操作就是把标签渲染到点云上看效果。最简单的可视化方式是利用 devkit 内置渲染:

nusc.render_sample_data( sample['data']['LIDAR_TOP'], with_lidarseg=True, verbose=True )

这种方式适合快速检查一帧数据是否正常。但我个人更推荐直接把点云和语义标签加载出来,用 Open3D 自己画,因为可以自由控制相机位置和颜色方案:

import open3d as o3d from nuscenes.utils.data_classes import LidarPointCloud pc = LidarPointCloud.from_file( os.path.join(nusc.dataroot, sample_data['filename']) ) # pc.points 是 (4, N) 的数组,前三行是 x, y, z points = pc.points[:3, :].T # 简单的 jet colormap,按类别 ID 上色 colors = label_to_color(semantic_labels) # N x 3, 自己实现映射 pcd = o3d.geometry.PointCloud() pcd.points = o3d.utility.Vector3dVector(points) pcd.colors = o3d.utility.Vector3dVector(colors) o3d.visualization.draw_geometries([pcd])

label_to_color可以自己写,也可以参考 devkit 里get_colormap的实现。官方颜色表比较讲究,不同类别区分度很高,建议直接用官方色表,不要自己随机配色,否则类别一多肉眼根本分不出来。

3.4 点云范围与坐标系注意事项

lidarseg 标签直接建立在 LIDAR_TOP 的原始坐标系下,不需要额外做坐标变换就能和点云对齐。但如果你要把 lidarseg 标签投影到图像上,或者和其他传感器数据融合,就必须经过calibrated_sensor和ego_pose的变换了。

我实测下来有个经验:在做点云裁剪或降采样时,一定要同步对标签数组做同样的操作。很多人在预处理阶段只对点云做了体素降采样,忘了对标签做对应采样,导致后续 loss 计算时点数和标签数对不上,跑起来直接报错。这个坑在多人协作的项目里尤其常见,最好在数据加载函数里就封装成"点云 + 标签一起变换"的原子操作,从源头避免错位。

4. panoptic 数据格式与加载实操

4.1 全景分割的标注编码

panoptic 和 lidarseg 最大的区别在于存储编码。panoptic 的 bin 文件每个点存的是一个uint16整数,编码方式分成两部分:低 8 位是语义类别 ID,高 8 位是实例 ID。用公式表示就是:

panoptic_label = (instance_id << 8) | semantic_id

这句话是整个 panoptic 数据格式的核心,不理解它就没法正确解析。读取时先用dtype=np.uint16读入,然后按位拆分:

panoptic_labels = np.fromfile(panoptic_bin_path, dtype=np.uint16) semantic_ids = panoptic_labels & 0xFF instance_ids = panoptic_labels >> 8

拆完之后,semantic_ids就是每个点的语义类别,instance_ids就是每个点的实例编号。由于instance_id只在同一语义类别内有意义,不同类别之间即使实例 ID 相同也互不影响。

4.2 thing 和 stuff 类别的差异

全景分割会把类别分成 thing 和 stuff 两大类:

  • thing 类指可数的、有独立实例的物体,比如 car、pedestrian、bicycle、traffic_cone,它们可以有多个实例,不同实例通过 instance_id 区分。
  • stuff 类指不可数的背景元素,比如 driveable_surface、sidewalk、terrain、vegetation,它们没有明确实例边界,instance_id 固定为 0。

区分这两类在训练和评估时非常重要。对于 stuff 类,模型只需要输出语义标签就行,不需要预测实例 ID;对于 thing 类,模型既要预测语义,又要区分不同实例。很多开源模型在实现 panoptic head 时,会专门针对 thing 和 stuff 设计不同的分支和 loss,比如 thing 分支用实例分割 loss,stuff 分支用语义分割 loss,这是 panoptic 任务的核心难点之一。

4.3 从 uint16 编码到可视化

下面给一个完整的 panoptic 加载和可视化示例:

# 假设 panoptic_bin_path 为某帧 panoptic 文件路径 panoptic_labels = np.fromfile(panoptic_bin_path, dtype=np.uint16) semantic_ids = panoptic_labels & 0xFF instance_ids = panoptic_labels >> 8 # 用语义 ID 上色,并叠加实例边界 colors = semantic_colormap[semantic_ids] # N x 3 # 也可以给同一个实例分配略有变化的颜色,突出实例差异 # 这里简单展示:对 thing 类,按 instance_id 调整亮度 thing_mask = np.isin(semantic_ids, thing_category_ids) if thing_mask.any(): # 将实例 ID 映射到一个 0-1 的因子,并作用到颜色上 inst_factor = (instance_ids[thing_mask] % 10) / 10.0 colors[thing_mask] *= (0.6 + 0.4 * inst_factor[:, None])

从可视化效果来看,做了什么处理一目了然:stuff 类是大色块,thing 类内部有明显的亮度差异,代表不同的物体实例。

4.4 与 3D 检测框的对应关系

不少朋友会问,panoptic 的实例能不能和 3D 检测框对应起来?严格来说二者不是一套标注体系。3D 检测框来自sample_annotation,用中心点、尺寸和朝向表示物体,而 panoptic 的实例来自点级标注,没有显式的物体中心或朝向量。同一个物体在两套体系里没有直接的主键关联,只能靠空间位置做启发式匹配。

这是一个很重要的认知。如果你打算做"检测 + 分割"的多任务融合,就需要自己设计从 bbox 到实例 mask 的匹配逻辑。有的人用点在框内做 voting,有的人用类别和中心距离做匹配,效果各有优劣。这里我不展开算法细节,但提醒一句:尽早意识到两套标注之间的不对齐,能帮你少走很多弯路。

5. 评估指标:mIoU 与 PQ、SQ、RQ

5.1 lidarseg 的 mIoU 计算

lidarseg 的标准评估指标是 mean Intersection-over-Union(mIoU),逐类别计算预测与真值的交集和并集,然后取平均。公式很简单:

IoU_class = TP_class / (TP_class + FP_class + FN_class) mIoU = mean(IoU_class for class in valid_classes)

需要注意排除 ignore 类。在实现 mIoU 时,我建议直接使用官方评估脚本里的逻辑,逐类别统计 confusion matrix,而不是自己写循环逐点判断,否则很容易写出低效且边界处理有 bug 的版本。

5.2 panoptic 的 PQ 分解

panoptic 的官方指标是 Panoptic Quality(PQ),由 Segmentation Quality(SQ)和 Recognition Quality(RQ)两部分组成,公式为:

PQ = SQ * RQ

其中 SQ 衡量匹配上的 segment 的平均 IoU,RQ 衡量匹配的召回精度,类似 F1 的思想。具体匹配规则是预测 segment 与真值 segment 按 IoU 超过 0.5 进行唯一匹配,一个 segment 只能被匹配一次。

PQ 对 thing 和 stuff 的处理略有不同。对 thing 类,每个实例是一个独立 segment,匹配要同时看语义和实例;对 stuff 类,通常按语义类别聚合,不区分实例。

5.3 用官方工具计算指标

官方 devkit 提供了完整的评估脚本,你只需要把预测结果按同样的 bin 格式导出,然后调用评估入口即可。基本流程是:

  • 准备预测文件夹,里面每个 bin 文件对应一个 keyframe 的 panoptic 预测,编码格式与官方一致。
  • 调用官方评估脚本,同时传入数据集版本、格路径、预测文件夹路径和输出目录。
  • 评估完成后读取 JSON 结果,里面包含每类 PQ、SQ、RQ 以及整体的 mIoU。

我用过几轮官方评估,发现一个细节:预测文件中不要漏掉任何 keyframe,漏一个文件会导致整个评估任务因为找不到对应预测而中止。还有,如果评估报维度不一致,优先检查预测文件是否是用uint16写入的,以及是否保持了和点云相同的点序。这两个问题占评估失败原因的八成。

6. 训练与使用中的常见问题

6.1 点序一致性是生命线

lidarseg 和 panoptic 标签文件本身没有点坐标,所有信息都和点云的点序绑定。一旦点云被预处理模块改动了顺序,标签就必须用完全相同的规则做 permutation。具体到工程实现,推荐把点云和标签保存在同一个数据类里,任何对点云的索引、删点、降采样操作都同时作用于标签。

这个要求听起来简单,实际项目里翻车率极高。比如有人为了方便,把点云从 (N, 4) 转成 (4, N) 时标签没跟着转置;有人做随机翻转增强时只翻了点云没翻标签;有人用torch.utils.data.DataLoader做 batch 时,collate 顺序不一样导致错位。这些坑都需要通过单元测试来兜底,比如加载一帧数据后,随机 shuffle 一个索引,验证点云和标签是否同步变化。

6.2 标注只覆盖 keyframe

nuScenes 的 lidarseg 和 panoptic 标注只覆盖samples/目录下的 keyframe,也就是大约每 2 秒一帧,sweeps/下的中间帧没有点级标注。这在训练时影响很大:如果你把 sweeps 也当成训练数据,必须自己生成伪标签,或者只计算 lidar 分支的 loss,否则会因为没有真值而报错。

6.3 类别不平衡与采样策略

nuScenes 点云语义标注存在明显的类别不平衡,车辆和道路表面占据绝大多数点,而像 traffic_cone、personal_mobility 这类小物体点非常少。直接按原始分布训练,模型会对这些稀有类别严重欠拟合。常用的做法有:

  • 在 loss 里使用类别权重,稀有类权重设高。
  • 对点云做类别重采样,比如限制每帧中占主导类别(如 driveable_surface)的点数。
  • 使用 focal loss 或 Lovasz-Softmax 这类对类别不平衡更鲁棒的损失函数。

我自己做实验时发现,光调 loss 还不够,数据层面的类别均衡也很有用。可以按类别统计点频率,生成一个采样权重表,在 DataLoader 中每次取点云时先按权重采样一部分点。这样模型对不同类别的表现会更稳,尤其是对 mIoU 的贡献更明显。

6.4 devkit 版本与数据集版本必须对齐

nuScenes devkit 更新频率不低,旧版本对 lidarseg 和 panoptic 的支持可能有 bug 或缺少某些接口。遇到诡异问题,比如NuScenes类里没有lidarseg属性,大概率是 devkit 版本太旧,建议升级到和数据集版本匹配的 release 版本。反过来,也不要盲目用最新版,因为接口变更可能导致你的数据预处理代码失效。我的习惯是记录当前项目使用的 devkit 版本号,并在 requirements.txt 里固定下来。

7. 最后的实操体会

这篇内容在实际应用中我反复验证过几次,最后分享两个最值得记住的经验:第一个是不要试图绕开官方 bin 格式,很多人想自己设计一个"更高效"的存储方式,结果徒增转换成本。直接按官方 uint8 和 uint16 格式加载,配合 numpy 和 numba,性能已经足够好。第二个是理解任务比跑通代码更重要,lidarseg 和 panoptic 的差异看似只是多了一个 instance_id,实际上从模型设计、loss 设计到评估方式都完全不同,先花时间把官方 JSON 索引和 bin 编码彻底吃透,后面你会省下很多 Debug 的时间。

如果你正在做 3D 感知相关的工作,建议把这套数据流程沉淀成自己团队的基础工具库,封装加载、可视化、增强、评估这些通用模块。后续想复用 nuScenes 的标注做其他任务,比如 occupancy prediction 或 BEV 分割,也能直接在上面扩展,收益会非常大。

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

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

立即咨询