ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

nuScenes lidarseg与panoptic实战:数据格式、加载与评估指南

nuScenes lidarseg与panoptic实战:数据格式、加载与评估指南 上一篇文章我们把 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(versionv1.0-trainval, dataroot/data/nuscenes, verboseTrue) 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, dtypenp.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。评估时必须排除类别 0ignore。很多新手直接对所有类别算 mIoU结果 ignore 点占比一高整个指标就失真。官方评估脚本会自动排除 ignore你自己实现评估时也要记得做 mask。3.3 点云逐点可视化拿到语义标签后最常见的操作就是把标签渲染到点云上看效果。最简单的可视化方式是利用 devkit 内置渲染nusc.render_sample_data( sample[data][LIDAR_TOP], with_lidarsegTrue, verboseTrue )这种方式适合快速检查一帧数据是否正常。但我个人更推荐直接把点云和语义标签加载出来用 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 数据格式的核心不理解它就没法正确解析。读取时先用dtypenp.uint16读入然后按位拆分panoptic_labels np.fromfile(panoptic_bin_path, dtypenp.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 分支用实例分割 lossstuff 分支用语义分割 loss这是 panoptic 任务的核心难点之一。4.3 从 uint16 编码到可视化下面给一个完整的 panoptic 加载和可视化示例# 假设 panoptic_bin_path 为某帧 panoptic 文件路径 panoptic_labels np.fromfile(panoptic_bin_path, dtypenp.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、RQ5.1 lidarseg 的 mIoU 计算lidarseg 的标准评估指标是 mean Intersection-over-UnionmIoU逐类别计算预测与真值的交集和并集然后取平均。公式很简单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 QualityPQ由 Segmentation QualitySQ和 Recognition QualityRQ两部分组成公式为PQ SQ * RQ其中 SQ 衡量匹配上的 segment 的平均 IoURQ 衡量匹配的召回精度类似 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 标注只覆盖 keyframenuScenes 的 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 分割也能直接在上面扩展收益会非常大。
返回列表