尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

nuScenes点云分割实战:lidarseg与panoptic标注解析及避坑指南

发布时间:2026/9/29 7:29:03

资讯中心
01
ARTICLE

nuScenes点云分割实战:lidarseg与panoptic标注解析及避坑指南

nuScenes点云分割实战:lidarseg与panoptic标注解析及避坑指南
nuScenes这座矿山我在系列第一篇里已经带大家把整体脉络理清楚了传感器布局怎么回事、坐标系统怎么转换、sample和scene怎么组织。今天这篇是系列的第二篇专门讲激光雷达点云层面两件非常关键的事lidarseg和panoptic。这两个任务都是给激光雷达点云做密集标注——lidarseg负责给每个点打上语义类别panoptic在语义的基础上再给每个点分配一个实例编号。做3D语义分割、实例分割、全景分割或者想在点云上做多传感器融合特征提取的朋友这篇应该能帮你省下不少翻文档的时间。文章会从任务定义、数据格式、加载代码、可视化、评估指标一路讲到常见坑位尽量做到拿到手就能跟练。特别要提醒一句nuScenes的lidarseg和panoptic标注下载方式和一般图像数据集差别挺大很多人第一步就卡在数据下载上所以这一篇也会把下载环节单独拿出来讲透。1. lidarseg和panoptic到底在解决什么问题1.1 激光雷达感知链路上这是最“地基”的一环摄像头有语义分割激光雷达同样需要。自动驾驶里激光雷达的职责主要是测距、测速、目标检测但仅仅知道“这里有一坨反射点”是远远不够的下游决策需要知道这坨点到底是车、是行人、是路沿还是路边灌木。lidarseg就是干这个的对每个激光雷达点给出一个语义标签类似图像领域的语义分割只不过数据从像素换成了三维点。panoptic则是在lidarseg上的自然延伸。你想想语义分割只区分“这一类”和“那一类”但路上五辆车都标成car对下游跟踪、运动预测来说信息量是不够的——我需要知道“这五辆每一辆的边界在哪”。于是panoptic任务要求同时给出语义类别和实例编号属于同一个物体的点共享一个ID不同物体ID不同。对不可数类别路面、地形、植被这类stuff只需要语义标签不需要区分实例对可数类别车、人、自行车这类things必须区分到每个实例。这套设计其实和图像全景分割完全同构。做过2D分割的人学这个会非常顺脑子里直接平移就行。1.2 nuScenes的lidarseg标注结构上做了哪些设计nuScenes官方在2021年发布了nuScenes-lidarseg数据集给1000个场景中约4万帧关键帧的激光雷达点云做了逐点标注点云来源是车顶的32线激光雷达。标注覆盖了8个大类、十多个小类从可数物体车辆、行人、自行车等到不可数区域可行驶路面、人行道、地形、植被等都有。官方在设计上最值得点赞的一点是给出了一个统一的lidarseg.json配置里面定义了每个类别的名称、索引和颜色。实际使用中我强烈建议直接从这个文件里读类别表而不是从网上抄一份类别清单。原因很简单不同split开放的小类数量不一样mini版本只开放了部分高频类别trainval版本的小类更全而且同一类别的index在不同版本之间可能有调整。你从网络博客抄的索引很可能和当前版本对不上加载出来颜色错乱、MIOU算出来离谱都是常见的后续连锁反应。存储层面lidarseg标注是每个sample关键帧一个二进制文件文件名对应sample_token内容是uint8类型的数组数组长度和该帧LIDAR_TOP点云的点数严格一致第i个值就是第i个点的类别索引。数据量很小单帧几十KB到几百KB不等比点云主文件小得多。1.3 panoptic并不神秘本质是“语义实例”的编码nuScenes panoptic标注是在lidarseg基础上扩展而来的官方给它的定位是panoptic lidar segmentation的完整版。标注文件同样是每个sample一个二进制文件但数据类型从uint8变成了int32每个点的标签用32位整数同时编码语义类别和实例ID高16位存instance_id低16位存semantic_label。读取的时候做一次位运算就能拆出来。这里要特别理解一个设计巧思它没有为panoptic单独发明新的标签体系而是复用了lidarseg的semantic_label只是额外叠加了instance维度的信息。这意味着你训练lidarseg的模型权重完全可以拿来初始化panoptic模型特征层面天然兼容。我实测下来这种复用对训练收敛速度的提升很明显尤其是stuff类别的语义特征几乎不需要重新学。对做下游任务的人来说panoptic的价值在于它打通了“检测”和“分割”之间的断层。检测给的是矩形框框里可能包含多个物体panoptic直接给的是每个点归属哪个具体实例这让后续的轨迹关联、目标跟踪能够直接在点云层面做而不用先依赖检测框。2. 数据准备下载、目录与加载2.1 先把这个数据集搞到本地很多人在这一步就卡住了。nuScenes的lidarseg标注不是打包在常见的主数据集包里的它需要单独下载。官方提供了两条路一是通过nuscenes-devkit里的下载接口二是在官网手动下载。mini版本整体不大我建议第一次跑通流程用mini就够了trainval完整版要将近千GB除非你真的要训练完整模型否则没必要一开始就下。下载前先装好依赖pip install nuscenes-devkit然后写一个简单的下载脚本from nuscenes import NuScenes # 先加载主数据集这一步会检查dataroot下的目录结构 nusc NuScenes(versionv1.0-mini, dataroot/data/sets/nuscenes, verboseTrue) # 下载lidarseg标注 nusc.download_version(splitmini, dst_dir/data/sets/nuscenes)执行后devkit会把对应的lidarseg压缩包下载下来并解压到正确位置。需要注意download_version内部走的是AWS S3网络环境如果对海外链路不友好经常会出现下载到一半就断的情况。我自己更常用的方式是在官网的下载页面用浏览器或aria2等多线程工具把对应的压缩包下下来再手动解压到dataset目录这样更可控。个人建议目录结构按官方推荐组织/data/sets/nuscenes/ ├── v1.0-mini/ │ ├── sample/ │ ├── sweeps/ │ ├── maps/ │ └── ... ├── lidarseg/ │ └── v1.0-mini/ └── panoptic/ └── v1.0-mini/如果你只有一个版本或者多个版本混用目录规划越规整后面写代码时出错的概率越低。2.2 目录结构与二进制格式说明下载解压完成后lidarseg目录下自然就是一堆以sample_token命名的.bin文件。panoptic目录同样。它们和主数据集里的sample_data通过token关联逻辑关系是这样的一个scene包含多个samplesample是带标注的关键帧每个sample的data.LIDAR_TOP指向一帧激光雷达点云每个sample对应的lidarseg标注文件名就是sample_token这种“文件名即token”的设计在nuScenes里到处都是但第一次接触的人容易把sample_token和sample_data_token搞混。这两个是不同的东西sample_token标识的是某一时刻的快照包含6个相机、5个雷达、1个激光雷达的数据sample_data_token标识的是具体某一个传感器文件。lidarseg文件名用的是sample_token不是LIDAR_TOP的sample_data_token。二进制格式上再强调一遍lidarsegdtypeuint8每个值直接是类别索引panopticdtypeint32低16位是语义标签高16位是实例ID读panoptic时如果直接打印原始数值你会看到很多很大的整数这是正常的不是数据坏了。很多人第一次读出来“上千万”的数字还以为标注有问题其实就是没拆位。2.3 加载一帧lidarseg标注的完整代码先看一段最朴素的加载代码理清楚API调用和数据流向import os import numpy as np from nuscenes import NuScenes nusc NuScenes(versionv1.0-mini, dataroot/data/sets/nuscenes, verboseTrue) # 取第一帧sample sample nusc.sample[0] sample_token sample[token] # 官方API直接得到lidarseg标注文件路径 lidarseg_path nusc.get_sample_lidar_seg(sample_token) seg_labels np.fromfile(lidarseg_path, dtypenp.uint8) print(点数:, len(seg_labels)) print(标签范围:, seg_labels.min(), seg_labels.max()) print(类别计数:, np.bincount(seg_labels))如果一切正常你会看到点数刚好等于该帧点云长度标签范围在0到类别数减1之间。接下来把点云也读进来验证一下对应关系from nuscenes.utils.data_io import get_sample_lidar_top pc get_sample_lidar_top(nusc, sample_token) points pc.points[:3, :].T # N x 3 print(点云点数:, points.shape[0]) print(标注点数:, seg_labels.shape[0]) assert points.shape[0] seg_labels.shape[0], 长度不一致这段assert基本每周都能帮科研群里的朋友排查出问题。长度不一致通常意味着版本不匹配比如主数据集是v1.0-trainval却加载了v1.0-mini的lidarseg标注。读取panoptic的区别很小就两行拆位代码panoptic_path nusc.get_sample_panoptic(sample_token) panoptic np.fromfile(panoptic_path, dtypenp.int32) semantic_label panoptic 0xFFFF # 低16位是语义类别 instance_id panoptic 16 # 高16位是实例ID print(语义标签样例:, semantic_label[:20]) print(实例ID样例:, instance_id[:20])注意semantic_label里是类别索引数值范围通常很小0到十几而instance_id是实例索引同一类别下的不同物体数值不同。对stuff类别来说instance_id位基本都是0因为官方规定不可数类别不做实例区分。这一点在可视化时特别重要否则你会画出六亲不认的噪声图。3. 可视化实操让点云“有颜色”3.1 直接调官方explorer的画图接口数据读进来了第一件事自然是可视化确认。NuScenes官方explorer里其实已经封装好了lidarseg渲染方法新手可以直接用nusc.explorer.render_pointcloud_in_birdseye_with_lidarseg(sample_token)这条命令会画出一张BEV俯视图点云按不同类别着色。官方接口的好处是零成本、无需维护颜色映射适合快速定性检查。但它的缺点也很明显交互性差、角度固定、点密密麻麻没法缩放而且如果你要批量把几十个场景的可视化结果拼成视频用官方接口会很痛苦。我的建议是官方接口用来入门可以但真想做好可视化分析最好自己写一个轻量渲染函数后面调模型、debug都能用得上。3.2 自己动手画BEV俯视图自己画BEV其实不复杂核心逻辑就三步读点云、读标签、按类别着色。下面这段代码我一直在用Mini和Trainval版本都兼容import numpy as np import matplotlib.pyplot as plt def render_lidarseg_bev(nusc, sample_token, filter_range50): # 1. 读取点云和标注 pc get_sample_lidar_top(nusc, sample_token) points pc.points[:3, :].T seg_labels np.fromfile(nusc.get_sample_lidar_seg(sample_token), dtypenp.uint8) # 2. 过滤太远的点BEV看着更清爽 mask np.linalg.norm(points[:, :2], axis1) filter_range pts points[mask] labels seg_labels[mask] # 3. 从lidarseg.json读颜色映射 lidarseg_path os.path.join(nusc.dataroot, lidarseg, nusc.version) with open(os.path.join(lidarseg_path, lidarseg.json), r) as f: meta json.load(f) categories meta[categories] color_map np.zeros((len(categories), 3), dtypenp.float32) for cat in categories: color_map[cat[index]] np.array(cat[color]) / 255.0 # 4. 画图 fig, ax plt.subplots(1, 1, figsize(10, 10)) ax.scatter(pts[:, 0], pts[:, 1], ccolor_map[labels], s0.5, alpha0.8) ax.set_xlim(-filter_range, filter_range) ax.set_ylim(-filter_range, filter_range) ax.set_aspect(equal) ax.grid(True, linestyle--, alpha0.3) plt.show()这段代码有几个细节我特意做了取舍过滤50米范围内的点画出来既保留自车周围的细节又不会变成一团毛线。如果双向场景想多看一些filter_range调到80或100即可点大小设置成0.5点云密集时这个数值观感最舒服直接从lidarseg.json读颜色保证任何版本下颜色和类别永远严格对应3D散点图版本也顺手给出来对观察遮挡关系和垂直结构帮助更大from mpl_toolkits.mplot3d import Axes3D fig plt.figure(figsize(12, 10)) ax fig.add_subplot(111, projection3d) # 只取前30米范围避免背面点把视角全部糊死 mask np.linalg.norm(points, axis1) 30 ax.scatter(points[mask, 0], points[mask, 1], points[mask, 2], ccolor_map[labels[mask]], s0.2) ax.set_xlabel(X (m)) ax.set_ylabel(Y (m)) ax.set_zlabel(Z (m)) ax.view_init(elev25, azim135) plt.show()3.3 可视化时我常用的几个调节手法实践里可视化踩过的坑比训练还多我总结几个高频经验。第一一定要区分“颜色映射错误”和“模型预测错误”。如果你用的类别索引是从网上文章里复制的很可能会把car的红色画到路面上去。排查技巧很简单取一帧已知地面上有车的数据看车上点数多不多、路面颜色是否均匀。如果路面颜色里混着车类颜色的点大概率不是标注问题是颜色映射表索引错了。第二视频可视化要固定坐标范围。批量做视频时每一帧的self-localization会微调如果你让Matplotlib自动缩放坐标轴视频会闪得人头晕。统一设置xlim、ylim画面会稳定非常多。第三BEV视图里把点云的z轴信息丢掉确实会损失一些信息。我的处理办法是做一个height overlay把z轴归一化作为亮度权重叠加到类别颜色上这样既能区分低矮的路沿和高处的路灯又不牺牲类别辨识度效果比我试过的很多花哨方案都好。第四想看清panoptic的实例分割效果不要直接按instance_id着色那样色差太碎。比较好的做法是先按semantic_label着色再给things类别的点叠加一个按instance_id生成的灰度扰动。这样同类别不同实例之间有轻微差异但整体画面依然干净。4. 评估指标mIoU与PQ怎么算4.1 语义分割的mIoU计算训练lidarseg模型官方指标用mIoUmean Intersection over Union。这个指标学图像分割的人都熟但点云版本有几个需要注意的坑。IoU的定义是某一类别的预测结果和真值交集的点数除以并集的点数。假设类别c的混淆矩阵元素为TP真值是c预测也是cFP真值不是c预测是cFN真值是c预测不是c那么IoU(c) TP / (TP FP FN) mIoU mean(IoU(c)) over all classes用numpy就能快速算出来def compute_miou(pred, gt, num_classes): miou 0.0 for c in range(num_classes): tp np.sum((pred c) (gt c)) fp np.sum((pred c) (gt ! c)) fn np.sum((pred ! c) (gt c)) denom tp fp fn if denom 0: miou tp / denom return miou / num_classes实际使用中还要注意ignore_index类别的处理、类别不平衡导致的极端IoU值、以及点云稀疏区域远处的低置信度点对指标的拖累。nuScenes官方评估脚本对这些细节做了处理所以直接上报官方渠道时你用官方脚本就好。但自己在训练日志中看涨跌时我建议使用自己写的轻量版这样不受官方评估脚本里评测集加载逻辑的限制迭代更快。4.2 全景分割的PQ、SQ与RQpanoptic任务用的核心指标是PQPanoptic Quality它分了两个维度分割质量SQ和识别质量RQ。官方思路的核心是“先匹配实例再算质量”而不是像语义分割那样逐像素硬算。公式长这样SQ sum(IoU over matched instances) / matched_count RQ matched_count / (matched_count 0.5 * unmatched_pred 0.5 * unmatched_gt) PQ SQ * RQ从另一个等价表达式看PQ sum(IoU over matched instances) / (matched_count 0.5 * unmatched_pred 0.5 * unmatched_gt)为什么PQ要这么设计因为单独用mIoU评估全景分割有一个致命缺陷mIoU只关心语义分类是否正确不关心实例切分是否正确。比如你把两辆紧挨着的车预测成一坨语义类别都是carmIoU可能还很高但PQ会因为实例没有匹配上而大幅下降。反过来如果你把一辆车预测成两半语义可能依然全对但实例匹配会失败RQ立刻掉下来。在nuScenes panoptic评估里算法会把预测实例和真值实例做二分匹配匹配条件通常是类别一致且IoU超过0.5。匹配完成后所有匹配上的实例才参与SQ计算没匹配上的预测会被当成误检没匹配上的真值会被当成漏检。这个机制对模型实例切分能力的要求非常直接。下表把两类指标的核心差异再捋一遍对比维度mIoUPQ评估粒度逐点实例是否区分实例不区分区分惩罚切分错误几乎不惩罚显著惩罚适用任务lidarsegpanoptic是否部分基于匹配否是4.3 跑一遍官方评估脚本nuScenes官方在devkit中提供了lidarseg和panoptic的评估入口。评估前需要把模型预测整理成官方规定的格式预测结果放一个文件夹内部每个文件命名为{sample_token}_lidarseg.bin或panoptic.bin内容是uint8或int32数组顺序和点云顺序一致同时提供一个result.json描述提交信息。以lidarseg为例评估代码是python -m nuscenes.eval.lidarseg.evaluate \ --result_path ./results/lidarseg_val \ --eval_set val \ --dataroot /data/sets/nuscenes \ --version v1.0-trainval \ --output_dir ./eval_output跑完后日志里会打印每个类别的IoU以及mIoU。第一次跑通时你会立刻感受到点云标注和图像评估的一个差异类别之间的IoU方差大得惊人高频且体积大的类别比如car、drivable surface动辄0.8以上体积小又稀疏的类别比如traffic cone可能只有0.2。因此只看mIoU不看逐类IoU的话很多问题会被平均值掩盖住。panoptic的评估入口类似python -m nuscenes.eval.panoptic.evaluate \ --result_path ./results/panoptic_val \ --eval_set val \ --dataroot /data/sets/nuscenes \ --version v1.0-trainval \ --output_dir ./eval_output输出内容会比lidarseg多一张各things类别的PQ汇总表排列维度是“类别 匹配/漏检/误检统计”。我一般最先扫PQ最低的三个things类别因为那基本上就是模型实例切分的短板所在。5. 常见问题与避坑清单5.1 数据集下载与硬盘规划下载这个数据集最典型的坑是“下错位置”。很多人把lidarseg压缩包下发到临时目录就解压了结果所有代码都读不到。建议动手前先规划好目录把它当作项目工程的一部分去做而不是随便解压完事。另外完整trainval的lidarseg和panoptic标注虽然本身只有几十GB但主数据集非常大含所有sample和sweeps。如果只是为了搞懂标签格式和跑通流程mini版本完全够用。它体量小、类别覆盖面足够、跑评估也快等你在mini上把整个流程走顺了再考虑要不要上完整版。下载速度问题不用多说多线程工具比浏览器下载省心太多。压缩包下载完记得核对一下文件大小和解压结果避免下了一半就解压导致bin文件损坏。我遇到过最气人的一种情况是文件能解压、能读出来但某些帧的点数和点云对不上查了半天才发现是压缩包损坏导致某几个bin被截断了。5.2 路径、版本与权限类报错NuScenes加载时报错最多的是这几个报错现象常见原因处理方式Could not find dataset.jsondataroot下没有对应version的目录检查主数据集是否解压完整version mismatch加载了v1.0-mini却读取trainval标注统一version参数KeyError: sample_token自己拼的路径有误用官方API get_sample_lidar_segPermissionError下载后无读取权限chmod -R ur 处理好文件权限我有个百试百灵的定位方法遇到加载问题第一件事不是看代码而是打印nusc.table_root和nusc.dataroot两个属性确认NuScenes对象到底加载了哪个目录下的哪个版本。90%的路径类问题都能在这一步暴露出来。5.3 panoptic解码搞错位运算panoptic数据读取是重灾区我几乎每周都看到群里有人问“为什么panoptic标签数组里的值都是几千万”。原因就一个不知道int32的高低位编码。这里再敲一次重点semantic_label panoptic 0xFFFF instance_id panoptic 16顺序不能反、不能少。另外读取时dtype必须是np.int32你用np.int64读出来位运算的结果会完全对不上。这个小细节我实训中坑过一次之后就再也不敢凭记忆写panoptic读取了。还有一个容易被忽略的点stuff类别路面、地形等的instance_id恒为0如果统计时把所有instance_id0的点当成things你会莫名发现点数少了一大截。实际上things和stuff的划分也写在官方配置文件里洗数据前一定先确认好哪些类别是things。5.4 评估提交格式不符合官方要求提交评估结果踩坑的频率非常高形式包括文件命名少了下划线、保存成bin时dtype不对、预测数组长度和点云不一致、result.json格式不完整等。官方对评估结果的格式要求非常严格任何一条不满足都会直接报错或者静默算错。提交前我建议做一轮自检import numpy as np from nuscenes import NuScenes nusc NuScenes(versionv1.0-trainval, dataroot/data/sets/nuscenes) sample nusc.sample[0] token sample[token] pred_path f./results/lidarseg_val/{token}_lidarseg.bin seg np.fromfile(pred_path, dtypenp.uint8) pc get_sample_lidar_top(nusc, token) # 核心校验 assert seg.shape[0] pc.points.shape[1], 点数不一致 assert seg.dtype np.uint8, lidarseg结果必须是uint8 assert os.path.exists(./results/result.json), 缺少result.json这一套组合拳打下来评估环节能省下不少来回调试的时间。就我个人使用体验来说nuScenes这套lidarseg和panoptic标注的质量是业内少见的扎实。条目结构清晰、官方工具链完备、迷你版足以覆盖完整流程特别适合作为3D分割算法研究的平台。不过工具好用是一回事真要复现一个sota模型并跑出自己的结果里面细枝末节的坑还是多今天这篇基本把我在mini和trainval上踩过的坑都复盘了一遍。下一篇文章我打算讲讲怎么基于这套数据快速搭一个轻量baseline从数据加载到模型训练一条龙跑通需要的朋友可以留意。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。