1. 项目概览与全景分割方案选型
1.1 为什么是HRNet而不是其他分割模型
先说结论:如果你的全景分割任务对边缘精细度、小目标召回率有较高要求,HRNet确实比常见的编码器-解码器结构更适合做底子。我在这个项目里对比过DeepLabV3+、SegFormer和HRNet,最终选定HRNet作为骨干网络,原因很直接。
全景分割(Panoptic Segmentation)本质上要同时解决两个问题:语义分割(stuff类,如天空、地面、墙壁)和实例分割(thing类,如人、车、动物)。这两个任务有个天然矛盾——语义分割需要大的感受野和上下文信息,实例分割需要高分辨率的空间细节。像DeepLabV3+这种结构,虽然ASPP模块能捕捉多尺度上下文,但下采样倍数大,Encoder阶段特征图分辨率掉得太快,小目标的边缘信息在Decoder阶段很难无损恢复。SegFormer虽然Transformer结构能建模长距离依赖,但对算力要求高,在嵌入式设备上跑实时推理不太现实。
HRNet的杀手锏是全程保持高分辨率特征图,通过多尺度特征交互来融合语义信息和空间细节。它有四个分支并行,分辨率从1/4一直降到1/32,每个分支之间不断做特征交换(Fusion Module),这样高分辨率分支能持续从低分辨率分支获取语义信息,低分辨率分支也能反过来获得空间细节。这个设计正好把全景分割两个子任务的需求同时满足了。
我们项目实际跑下来,HRNet在Cityscapes验证集上,PQ(Panoptic Quality)比DeepLabV3+大概高出3到5个百分点。别小看这几个点,在全景分割这个任务上,PQ每涨1个点都很吃力,尤其是边缘区域的IoU提升非常明显。
1.2 全景分割与语义分割、实例分割的关系
很多刚开始接触全景分割的同行会混淆这几个概念,这里我用自己的话理一遍。
语义分割是把图像里每个像素归类到某个类别,比如人、车、天空,但同一类别的多个物体不会被区分,图像里十个人全标成"人"。实例分割则只关注thing类,对人、车这类可数物体逐个区分,给每个实例一个独立的边界框或掩码,但对天空、地面这类stuff类不处理。
全景分割是把两者统一起来:stuff类做语义分割,thing类做实例分割,最后把两者的输出合并成一张完整的、每个像素都有归属标签的分割图。每个像素不仅仅要知道它是什么类别,如果它是thing类,还要知道它属于哪个实例。这个"统一"听起来容易,实际做起来很难,因为两类任务的标注格式不同、网络输出头不同、后处理逻辑也不同,怎么把它们一致地融合是核心难点。
我项目的做法是一条独立的全景分割Head,同时输出语义分割结果和实例中心点预测,再结合一个轻量级的grouping模块做后融合。这样避免维护两套独立模型,推理速度也更快。
2. 数据标注工具选型与数据集构建
2.1 标注工具对比:LabelMe、CVAT、Supervisely、Labelbox
数据标注是全景分割项目中最耗人力的环节,没有之一。我前前后后试了四款工具,各有优劣,这里直接说干货对比。
LabelMe是开源工具里最常见的选择,优点是完全免费、格式简单,标注结果输出为JSON文件,每个多边形(Polygon)都有坐标点和标签。适合小规模验证,团队只有一两个人时效率还能接受。痛点很明显:没有实例ID的管理机制,一个类别的多个实例需要你自己在标签里手写额外字段区分,标注一个几十个车、十几个人交错的复杂场景,光是检查ID有没有标错就能耗掉一下午。其次,它没有内置的自动保存和冲突检测,多人同时标注同一个文件时,版本管理很痛苦。
CVAT是目前我用下来综合体验最好的开源工具,支持在线协作,可以几个人同时标同一张图的不同区域。它对全景分割的友好之处在于支持"标签+实例ID"的属性字段设定,比如你定义人这个类别,可以额外加一个"Instance_ID"属性,标注时每个实例自动递增。CVAT还能导出COCO Panoptic格式的JSON,这对后续训练非常方便,省去了写转换脚本的时间。缺点是学习成本稍高,界面功能多,新手第一次打开容易懵。
Supervisely和Labelbox都是商业工具,功能最全,内置了自动分割模型(比如PointRend、SAM的集成),标注时可以先用模型预标注,人工只需要修正边缘和拆分实例,人效提升非常明显。但代价是费用不低,而且数据需要上传到第三方平台,对数据敏感的团队来说需要额外评估合规风险。
我们项目最终选了CVAT,核心原因是它的COCO Panoptic格式导出功能太省事了。前前后后标注了三万多张图,涉及十五个类别,如果全部手动转格式,至少要腾出一个人三天时间专门写转换脚本和做数据校验。
2.2 标注规范与质量控制流程
工具只是第一步,标注规范才是真正影响模型上限的环节。全景分割标注最容易出的问题是边缘不闭合和重叠区域归属不明确,特别是多个同类物体挨在一起时(比如停车场里紧挨着的车),标注员之间标准不统一,训练出来的模型在物体交叠区域的表现会很糟糕。
我团队在标注前专门开了一个标准化会议,定了几条硬性规则:
- 实例掩码边缘必须闭合,不能留有缺口
- 两个相邻实例有遮挡时,被遮挡的边界沿着遮挡物边缘走,不能跨到遮挡物的像素区域
- 微小物体(比如远处不足10个像素的行人)统一标为忽略区域(Ignore),不参与训练
- 类别标签统一使用英文字符,一个类别只能有一个名字,禁止同义词
质量控制方面,样本抽检率10%,重点抽检遮挡密集场景和边缘复杂场景。第一次交付的标注结果抽检合格率只有82%,明显低于95%的目标线。后面改成了"预标注+二次修正"的模式:先用一个初版模型(在公开数据集上训练的)对图片做预标注,标注员在预标注结果上修正边缘和增删实例,效率提升了两倍多,抽检合格率也稳定在了95%以上。
注意:预标注虽然省力,但如果初版模型的效果太差,反而会误导标注员,让标注结果偏向模型的错误认知。建议预标注的模型先用一个已有数据训练到PQ达到40以上再上线,否则效果会适得其反。
3. HRNet模型训练与调优实战
3.1 网络结构与Loss设计
训练部分我基于mmsegmentation框架搭建的。骨干网络选HRNet-W48,输出头做了定制化改造,结构上分三个分支共享一个骨干:
- 语义分割分支:1x1卷积直接输出HxWxC_sem(C_sem为stuff类数量)
- 实例中心点分支:输出HxWx1的中心点概率图,用Gaussian峰来编码每个实例的中心
- 实例边界回归分支:输出HxWx2的偏移量,表示每个像素到所属实例中心的相对距离
这样设计的好处是,实例分割分支不需要做像素级的Embedding聚类,避免了传统方法里距离度量的设定难题。推理时只需要在后处理阶段找到中心点峰,然后做区域填充就能得到实例掩码。
Loss设计方面,总共三个Loss,语义分割用标准交叉熵Loss(配合OHEM处理类别不均衡),中心点分支用Focal Loss的变体,偏移回归分支用Smooth L1 Loss。总Loss三者的权重设为1:0.8:0.5。这个配比后面验证下来是比较稳的,中心点的权重稍高是为了保证小目标能够被正确召回。
3.2 训练参数配置与优化技巧
训练配置直接给大家参考:
- 输入尺寸:800x800(原始图片等比缩放后中心裁剪)
- Batch Size:32(8卡A100,每卡4)
- 优化器:AdamW,初始LR 2e-4,Poly策略衰减
- 训练轮数:60个Epoch(Cityscapes预训练权重基础上微调)
- 数据增强:随机翻转、随机裁剪、颜色抖动、CutMix(对小目标有奇效)
有几个坑要单独拎出来说。
第一,类别不均衡问题很严重。我们数据集中"人"和"车"占了85%以上,而"交通锥""自行车"这些类别的样本比例不足3%。单纯用交叉熵Loss训练,小类别的PQ甚至连10都到不了。解决办法是给每个类别设置一个权重系数,权重值与类别像素占比的倒数成正比,同时做了截断防止权重差异过大(上限不超过5倍)。这一招直接把交通锥的PQ从8.2拉到了31.5,提升非常显著。
第二,CutMix增强要谨慎开。在全景分割任务中,CutMix把一张图的区域切下来贴到另一张图上,会导致实例掩码出现截断区域。我们踩过坑后把CutMix的起始概率设成了0.2,并且只允许在stuff类区域做剪切,避开thing类区域,这样既保持了增强效果,又不破坏实例完整性。
第三,HRNet在高分辨率特征图上计算量大,800x800输入,W48版本,单卡推理一次前向就要3.5GB显存,八卡训练勉强不爆。如果你显存紧张,可以降输入尺寸到640x640,PQ大约掉1到2个点,但训练速度和显存占用会友好很多。
训练过程中的监控指标除了PQ之外,强烈建议同时盯着类别的PQ表格,不要只盯一个均值。有个很典型的例子:我们某次训练均值PQ涨了1.2,但打开分类别统计发现"公交车"的PQ从38掉到了25,因为新加的数据里公交车样本太少被其他类别淹没。如果不看细分类别,这个问题根本发现不了。
3.3 模型评估与精度分析
训练结束后,用COCO Panoptic的评估标准在验证集上做了完整评测。最终结果:
- PQ(Panoptic Quality):54.7
- SQ(Segmentation Quality):71.9
- RQ(Recognition Quality):75.8
对比初始版本的DeepLabV3+(PQ约50.2),HRNet版本在PQ上提升了4.5个点,主要贡献来自边缘区域IoU的提升和中等尺寸目标的识别率改善。小目标(面积小于32x32)的PQ提升了6.3个点,这个效果正是高分辨率分支带来的红利。
不过HRNet也有短板,对遮挡严重的密集场景处理不如基于Transformer的模型。我们的场景里偶尔会出现两辆紧挨的同色车,实例分割分支会偶尔把两辆车合并成一个实例。后面尝试在中心点分支后加了一个形态学分离后处理,效果有改善,但不能完全根治。
4. TensorRT推理优化完整实录
4.1 从PyTorch到ONNX再到TensorRT的转换流程
模型训练完之后,部署才是真正磨人的阶段。我们的目标是跑到30毫秒内(大约33FPS),硬件是Jetson Orin NX,显存8GB。
转换链路上走的是PyTorch→ONNX→TensorRT这条主流路线。具体操作分这几步。
第一步,把模型导出为ONNX格式。先用torch.onnx.export,注意设置opset_version=11,太高了TensorRT老版本不支持,太低了有些算子导出不了。HRNet里用到的Operator基本都兼容opset 11,但需要注意F.interpolate的坐标变换模式要导出为固定行为,建议直接设成"bilinear"对齐到Opencv的方式。
导出时还有两个细节必须处理:
- 把所有张量的维度全部固定。TensorRT对动态Shape支持不算友好,特别是涉及Resize和Pooling的层,动态Shape会让优化器无法做层融合。我们直接固定输入尺寸800x800,输出特征图维度也固定下来。这个项目不需要处理任意分辨率输入,所以固定Shape是性价比最高的选择。
- 把Batch维度固定为1。只有Batch=1时TensorRT才能做很多激进优化,包括层消除和内存复用。
第二步,用trtexec工具生成TensorRT引擎。命令行大致是:
trtexec --onnx=model.onnx --saveEngine=model.trt \ --fp16 --workspace=4096 \ --minShapes=image:1x3x800x800 \ --optShapes=image:1x3x800x800 \ --maxShapes=image:1x3x800x800显存充足的话workspace给大一点,层融合效果会更好。但我们板子上显存有限,给到4GB已经到极限了。
第三步,写推理代码加载TensorRT引擎。这里有个建议:如果你用Python做推理,用PyCUDA负责张量搬运会比TensorRT自带的Python API更灵活,可控性更高。
4.2 FP16量化实践与精度对齐
FP16是最简单也最主流的加速手段,我们的实测数据是:开启FP16后单帧推理时间从原来的43毫秒降到19毫秒,PQ在验证集上只掉了0.8个点。这0.8的损失主要集中在少数极端场景,属于可接受范围。
但是FP16有个隐藏的坑,HRNet的Fusion模块里有大量加法和拼接操作,这些操作在FP16下会引入轻微的精度漂移。特别是在训练时没有做量化感知(QAT)的情况下,模型某些权重值分布范围很大,FP16表示时尾部信息会丢失。
我的解决办法是分类次地处理:
import tensorrt as trt # 设置FP16的同时,把敏感层指定为FP32 # 通过TensorRT的Layer钩子来识别敏感层 def mark_layers(network, engine): for i in range(network.num_layers): layer = network.get_layer(i) if layer.name and ('fusion' in layer.name or 'smooth' in layer.name): layer.precision = trt.float32 layer.set_output_type(0, trt.float32)说白了就是把Fusion模块和边缘相关的高频信息层强制保留FP32,其他层用FP16。这样做的代价是推理时间稍微涨了2毫秒,但PQ基本追平了FP32水平(掉了不到0.2个点)。如果你的精度敏感度更高,这个方案值得一试。
4.3 端到端推理Pipeline设计
TensorRT引擎跑起来只是第一步,完整的全景分割推理流程还包含预处理、后处理、目标追踪等模块。我整体Pipeline是这样设计的:
- 图像解码和Resize到800x800(用OpenCV,双线性插值)
- 归一化:像素值除以255后,再用训练时的均值和方差做标准化
- TensorRT推理,输出三张特征图
- 后处理:语义分割分支做Argmax得到类别图;中心点分支通过峰值检测找到实例中心;偏移分支结合中心点做像素聚类;最后把两分支结果合并成全景图
- 实例ID稳定化:对相邻帧的实例做IoU匹配,保持ID不跳变
后处理部分的决策阈值也很关键。中心点检测的阈值设太低会让模型产生大量假阳性实例,设太高会漏检小目标。我在Calibration集上做了一个小范围搜索,最终阈值定在0.35,这个值在召回率和准确率之间取得了平衡。
整条Pipeline在Orin NX上的实测性能是全流程19毫秒推理+6毫秒预处理+8毫秒后处理,整体约33毫秒一帧,换算下来正好30FPS,满足了实时性要求。
5. 常见问题排查与部署经验速查
5.1 标注-训练-部署全链路问题排查表
我把这个项目踩过的所有问题整理成了一张速查表,遇到类似情况可以直接对号入座。
| 环节 | 问题现象 | 可能原因 | 解决方案 |
|---|---|---|---|
| 标注 | 标注文件导出的实例ID错乱 | CVAT属性字段类型设置成了字符串而非整型 | 标注前检查属性字段类型,统一为整数 |
| 训练 | 训练Loss下降但验证PQ不升 | 数据增强太强破坏实例完整性 | 关闭CutMix或只限制在stuff区域使用 |
| 训练 | 某个类别PQ特别低 | 类别样本严重不均衡 | 设置类别加权Loss,权重与像素占比倒数成正比 |
| 训练 | 模型对交叠物体分割成粘连一片 | 标注时实例边界不清晰 | 二次抽检时重点检查遮挡密集场景 |
| 转换 | ONNX导出报错或不支持某算子 | PyTorch版本和ONNX版本不匹配 | 固定PyTorch版本2.0.1,ONNX版本1.13.0 |
| 转换 | TensorRT构建引擎时报错 | Dynamic Shape导致的维度推断失败 | 输入尺寸固定,Batch固定为1 |
| 转换 | FP16精度损失明显 | 敏感层(Fusion、Smooth)被量化 | 手动指定这些层强制FP32 |
| 推理 | 单帧推理时间波动大 | 显存复用机制失效 | 检查是否频繁调用engine的destroy和重建,尽量复用Context |
| 推理 | 后处理阶段成为瓶颈 | Python循环处理像素过多 | 用GPU做峰值检测和像素聚类,或者用C++重写后处理 |
5.2 项目复盘:哪些环节最耗时间
整体复盘下来,这个项目从零到落地花了大约两个月,人员投入是2名开发加3名标注员。时间分布很能说明问题:
- 标注环节占了35%,这是全景分割项目永远绕不开的投入大头
- 训练调参占了20%,主要难点在类别不均衡和Loss配比
- TensorRT转换优化占了30%,远超我的预期
- 剩下的15%留给前后处理逻辑调试和项目文档
TensorRT这部分超预期的原因,主要是PyTorch导出ONNX时遇到了一些算子兼容性问题,以及FP16量化后的精度回退排查花了整整两天。这些都是常规文档里不会教你的东西,只能靠实际踩坑积累。
5.3 几个提高落地效率的独家经验
第一,标注阶段就同步开始训练一个初版模型。哪怕标注数据只有五百张,先跑出一个效果一般的模型,后续标注可以靠它做预标注入力修正,效率翻倍不算夸张。
第二,TensorRT优化不要一上来就开FP16。先把FP32引擎跑通,确保精度和PyTorch输出对齐,确认没问题后再开FP16。不然精度问题根本分不清是转换引入的还是FP16引入的。
第三,后处理如果用Python写,建议先分析清楚每个功能模块的耗时占比。我们这个项目一开始后处理要35毫秒,比模型推理本身还慢,定位后发现是逐像素遍历导致,后面把中心点检测和区域填充改成GPU算子,直接降到了8毫秒。
第四,工程上务必做好模型版本管理,不只是权重文件,要从数据标注版本、训练配置、TorchScript/ONNX导出脚本到TensorRT引擎全部绑定一个版本号。我后期就吃过亏,排查线上某个案例发现模型文件和当时部署的配置文件对不上,浪费了一天时间才定位到是版本混用了。
6. 后续优化方向与个人总结
这个项目目前达到了预期的精度和性能指标,但离完美还有距离。如果后续继续迭代,我个人会优先考虑这几个方向:
第一,尝试把HRNet的Fusion模块替换成轻量化的EMA(Efficient Multi-scale Attention)结构。HRNet精度高但参数和计算量都不小,EMA模块在保持多尺度交互能力的前提下能削减一部分计算复杂度,对边缘设备的帧率提升可能有明显帮助。
第二,加入时序信息。当前模型是逐帧独立分割,对视频里的闪烁和ID跳变没有任何平滑约束。后续可以尝试引入轻量的时域匹配网络,或者在后处理环节加上基于光流的ID传播机制。
第三,针对遮挡场景做专项优化。前面提到紧挨同类物体容易合并成单个实例,这个问题的根治可能要改损失函数,比如在中心点分支上额外加一个对重叠区域的惩罚项,强制模型学习区分距离极近的两个实例中心。
最后分享一个实操中的体会:全景分割和普通分割项目最大的区别在于,它的工程链条特别长,标注、训练、部署任何一个环节掉链子,整个项目都会被拖垮。建议做这个方向的朋友一定在项目启动前把标注规范、数据格式、部署硬件约束这三件事提前讨论清楚,宁可前期多花两三天时间做标准化,也不要等标注了一千张图之后回炉重做。另外TensorRT的踩坑过程虽然磨人,但经历过一次完整的转换优化后,后面再换任何模型都会顺手很多——这个能力是通用的,投进去的时间不算浪费。