前阵子做了一套基于Mapillary Vistas数据集的街景语义分割流程,从数据预处理、标注解析、模型训练到结果可视化,完整跑了一遍。这类需求在自动驾驶感知、高精地图要素提取、城市数字化这几个方向都有应用,实际接手时会发现,真正的麻烦不在“跑通模型”,而在把数据和需求理顺。这篇文章把我自己这套完整流程拆开讲清楚,包含Mapillary Vistas数据集的解构、模型选型的对比、训练参数的实际配置、以及交付阶段容易踩的坑,给正在或准备做同类项目的朋友一个可以直接参考的路线图。
适合看这篇内容的人有两类:一类是刚接触街景分割、想用公开数据集做实验的学生或开发者,另一类是接到“帮我把分割结果跑出来”这类需求、需要快速交付的从业者。难度取中间值,我默认你已经用过PyTorch、跑过基础的图像分类或检测项目;如果完全没有深度学习实操经验,建议先找个小数据集把训练-评估闭环跑通,再来看这篇会顺畅很多。
1. 街景语义分割:它到底解决什么问题
1.1 从一张街景图说起
语义分割的本质是给图像里的每个像素打标签。一张街景图中间是马路,两旁是建筑物,路边停着车,人行道上有行人,天空占了大片背景。分割模型要做的事,就是把“这是道路”“这是建筑”“这是车辆”“这是行人”“这是天空”精确到像素地分类出来。
它看起来像是图像分类的延伸,但实际完全不同。图像分类只需要一个全局判断,而分割要求在输出层保持空间分辨率,这对模型的感受野、信息融合方式和显存占用都提出了更高要求。一个能正常跑ImageNet分类的GPU配置,拿来跑分割很可能直接爆显存。
在真实场景里,街景语义分割的价值体现在几个方向上:
自动驾驶感知。车辆需要知道前方哪些像素是可行驶区域、哪些是障碍物。道路、车道线、路缘石这些要素的像素级分割,是路径规划和避障的重要输入。
高精地图要素提取。地图采集车拍到大量街景影像,需要自动提取出交通标志、信号灯、路面标记等要素。纯人工标注成本太高,分割模型是自动化管线里的核心环节。
城市数字化与规划。统计街道绿化覆盖率、建筑物占地面积、公共空间构成比例,如果用人工去逐张描边,效率极低。模型跑一遍,各类要素的面积占比直接就能算出来。
此外还有数字孪生、AR导航、视觉无障碍辅助(帮助视障人士识别路面情况)等应用方向。本质上,街景分割是把“图像”转化为“结构化场景描述”的第一步,后面不管接的是控制策略还是数据库,都需要这样一层像素级理解作为基础。
1.2 为什么Mapillary Vistas是绕不开的选项
做街景分割,数据集的重要性比模型更突出。
公开数据集常见的几个是Cityscapes、ADE20K、BDD100K、CamVid和Mapillary Vistas。如果目标是街景场景,Cityscapes名气最大,但它有一个明显短板:图像基本来自德国城市,场景多样性有限。深度学习模型对数据分布非常敏感,用Cityscapes训练的模型,拿到亚洲城市或北美郊区的街景上,泛化效果往往会打折扣。
Mapillary Vistas的优势在于全球视野。它收集了六大洲、多个国家的街景图像,覆盖不同地区的路网结构、交通标志样式、建筑风格和光照环境。也就是说,它的地域多样性明显更强,模型看到的数据分布更接近真实世界的整体变化。对需要跨地区落地街景分割能力的项目来说,这个数据集几乎是绕不开的。
规模上,Mapillary Vistas提供了约25,000张高分辨率街景图,其中训练集18,000张、验证集2,000张、测试集5,000张。标注类别为66个,但这不是一份平铺的类别清单,它内部做了层级和类型划分,包括物体类、物质类、实例类、孔洞结构等,这些都会直接影响数据加载和训练逻辑的写法。
拿到项目需求后,我第一轮一定会确认三件事:
第一,数据从哪来。Mapillary Vistas不是直接开放下载,需要在官网注册账号并同意使用条款后才能获取。对方是否已经拿到数据,决定了我先做数据处理还是先搭模型。
第二,跑在什么设备上。如果对方只有一张消费级显卡,模型选型和输入尺寸就必须往轻量化方向调整;如果有服务器级别显卡,可以直接上高分辨率输入。
第三,验收标准是什么。要的是mIoU达到多少,还是只需要一套能跑通的结果可视化?这个差异会导致整个项目的重心完全不同。
这三个问题看似简单,实际能过滤掉一大半无效需求。做项目最怕的不是技术难题,而是需求边界模糊。
2. 数据集深度拆解与预处理要点
2.1 数据规模、类别体系与文件结构
Mapillary Vistas的原始数据,我第一次拿到后的反应是“文件结构真不省心”。它分了training_images、training_gt、validation_images、validation_gt等目录,但训练集和验证集的标注并非简单的同名文件对应。
最大的特点在于:标注文件是JSON格式,不是直接给你一张五颜六色的标签图。每个JSON文件包含对象的polygon(多边形)描述,复杂对象还嵌套了hole结构。比如一辆车的轮廓里有一块被树干遮挡的区域,在标注里就体现为外层polygon和内层hole。加载时必须先遍历JSON,把所有多边形在一个全黑的mask上填充,才能得到真正的标签图。
我从这个项目里整理出的处理流程是:
- 解析JSON,读取每个object的标签类别和polygon坐标。
- 为每个对象生成唯一序号,在实例层面建立映射关系。
- 用绘图库把polygon按序号填充到单通道mask上。
- 根据类别列表,将实例序号映射回语义类别ID。
- 检查空洞、重叠等异常情况,输出与图像尺寸一致的标签图。
这个流程看着简单,真正跑起来会遇到很多细节问题。比如不同版本JSON里的字段名可能不一致,某些类别在训练集中出现过但验证集中没有,某些图像分辨率非常高(很多超过3000×3000),一次性把整个mask读入内存非常吃力。
66个类别的体系结构值得单独说。Mapillary Vistas把类别做了类型划分,我简化为最常用的几个组:
| 组别 | 典型类别 | 类型 |
|---|---|---|
| 地面类 | 道路、人行道、路肩、卵石路面 | 物质类(stuff) |
| 交通设施 | 交通标志、信号灯、路缘石、护栏 | 物体类(object) |
| 车辆类 | 小汽车、卡车、公交车、摩托车、自行车、拖车 | 物体类(instance) |
| 建筑与周边 | 建筑、栅栏、围墙、门 | 物质类/物体混合 |
| 自然环境 | 植被、地形、天空 | 物质类(stuff) |
| 人体相关 | 行人、骑行者 | 物体类(instance) |
stuff和object的区别在于:stuff是“没有固定形状的材质”,比如天空、道路;object是“有明确轮廓的个体”,比如车辆、行人。对object类可以额外做实例分割,对stuff类通常只做语义分割。如果你后续需要输出实例级结果,这个数据集的标注体系已经为它打了底。
2.2 与主流街景数据集的横向对比
动手前我习惯做一张对比表,把各数据集的特征摆在一起看,选型和解释都会方便很多:
| 数据集 | 图像数量 | 标注类别 | 分辨率 | 地域覆盖 | 标注形式 |
|---|---|---|---|---|---|
| Cityscapes | 约25,000(精细5,000+粗糙20,000) | 19 | 1024×2048 | 德国城市为主 | 像素级mask |
| Mapillary Vistas | 约25,000(训练18,000) | 66 | 多为高分辨率,常达3000像素以上 | 全球六大洲 | JSON多边形 |
| ADE20K | 约27,500 | 150 | 多种尺寸 | 场景通用 | 像素级mask |
| BDD100K | 10,000 | 19(分割任务) | 1280×720 | 美国多城市 | 像素级mask |
| CamVid | 701 | 11 | 960×720 | 英国剑桥 | 像素级mask |
Mapillary Vistas的核心优势是类别全、覆盖广、分辨率高。但这三个优势也对应三个代价:标注加载复杂、内存开销大、类别极不均衡。
类别不均衡是其中最典型的坑。道路和建筑在绝大多数街景图中面积占压倒性多数,而摩托车、自行车、拖车、行人这类小目标出现频率低、像素占比更小。如果直接用朴素交叉熵训练,小类别很容易被淹没,模型把所有像素都预测成道路都能拿到很高的准确率,但mIoU会低得离谱。这个问题我会在第4节展开讲。
2.3 高分辨率图像的处理策略
Mapillary Vistas很多原始图像分辨率在2000到3000像素甚至更高,直接整图送进模型,显存直接就爆了。我采用的方案是“随机裁剪训练 + 整图滑窗推理”。
训练阶段,不用整图,而是从训练样本中随机裁剪固定大小的patch,比如512×1024或640×1280。这个思路和用ImageNet训练分类模型时随机裁剪224×224本质相同,既能控制显存占用,又能通过随机位置增加数据多样性。与此同时,每次裁剪前对全图做随机缩放(0.5到2.0倍),进一步增加尺度多样性。
推理阶段则反过来,不能随机裁剪,否则同一个物体会因为被截断导致预测不一致。我用的方案是重叠滑窗:按stride = 窗口大小的一半在整图上滑动推理,最后对重叠区域按平均概率融合。窗口边界处往往存在预测不连续,重叠融合能显著减轻接缝效应。
标签映射也是一个必须处理的细节。Mapillary Vistas的原始label不是0到65连续编号,中间有空缺。训练时如果直接用原始ID做交叉熵,模型会去学习几个根本不存在的类别。我的做法是输出通道数固定在66,训练时使用连续的类别索引,验证时再把预测结果映射回原始标注体系的ID计算IoU。这一步处理不好,前后期指标和可视化结果会对不上。
3. 模型选型与训练流程实操
3.1 语义分割模型怎么选
数据集准备妥当后,模型选型是另一个关键决策。
目前街景分割常用的模型路线大致分三派:卷积派、Transformer派、混合派。
卷积派的代表是DeepLabV3+。它用空洞卷积扩大感受野,再加上ASPP模块(空洞空间金字塔池化),在多个尺度上聚合上下文信息。这套方案的优点是实现成熟、训练稳定、对数据量要求相对宽松,是默认选项级别的存在。我习惯拿它做基线,代码库现成,超参数参考多,跑出来结果稳定。
Transformer派以SegFormer为代表。它把图像切块后做自注意力,用分层结构保留多尺度特征,而且位置编码做了解耦,对输入分辨率不固定这件事天然友好。在Mapillary Vistas这种高分辨率、类别多的场景里,SegFormer的表现往往比同规模卷积模型更好,尤其是对远景小目标。缺点是训练技巧要求更高,某些情况下性能不稳定,需要细心调整。
混合派的代表是HRNet,它全程保持高分辨率特征,同时并行引入低分辨率分支,最终融合所有分辨率的信息。HRNet对边界细节的保留特别好,适合对边缘精度有高要求的场景,代价是计算和显存开销偏大。
如果是赶工型项目,我一般选DeepLabV3+做主力,先把训练时间和交付节点控制住。如果对方明确要求“指标尽可能高,时间不受限”,SegFormer或者带大骨干的DeepLabV3+才值得考虑。不同模型的适配情况我整理成表:
| 模型 | 骨干网络 | 推荐输入尺寸 | 训练集量级 | 速度 | 精度上限 | 适合场景 |
|---|---|---|---|---|---|---|
| U-Net | 轻量骨干 | 256×256 | 几千到几万 | 最快 | 中 | 小数据集、快速原型 |
| DeepLabV3+ | ResNet50/101 | 512×1024 | 数万 | 中 | 高 | 稳妥基线 |
| PSPNet | ResNet101 | 473×473 | 数万 | 中 | 高 | 全局上下文强的场景 |
| SegFormer | MiT-B2/B3 | 512×1024或更高 | 数万 | 中快 | 更高 | 高精度、高分辨率 |
| HRNet | HRNetV2-W48 | 512×1024 | 数万 | 较慢 | 很高 | 边界精细要求高 |
如果拿不准,先用DeepLabV3+配ResNet50把基线和预处理全部打通,再根据时间和资源决定要不要升级。项目交付里最忌讳一上来就追最高配模型,导致调试时间失控。
3.2 训练配置与关键参数解析
我拿一组实际调过的配置来说明,这套参数跑Mapillary Vistas训练集的子集效果稳定,对不同显卡的兼容度也不错:
- 输入尺寸:512×1024,显存紧张就降到384×768
- 随机裁剪:先对整图做0.5~2.0尺度缩放,再随机裁patch
- 优化器:SGD,momentum=0.9,weight_decay=0.0001
- 学习率:初始0.01,使用poly策略,power=0.9,即 lr = base_lr * (1 - iter/total_iters)^0.9
- 训练轮数:80到120个epoch
- 损失函数:CrossEntropyLoss,配合OHEM(在线难例挖掘)
- 归一化:ImageNet的mean/std
- 批大小:8,多卡按比例扩展
为什么用poly学习率而不是阶梯式衰减?因为语义分割的收敛曲线相对平滑,poly策略在训练后期依然可以精细调整参数,而阶梯式衰减容易在步长切换点附近引起损失跳变。这是训练细节,但直接影响最终指标。
OHEM的实现思路是:计算每个像素的交叉熵损失,选出损失最大的K个像素(比如top 20%)参与梯度回传。这样做让模型把注意力集中到难分类像素上,对小类别和边界区域尤其有效。我实测下来,在Mapillary Vistas这种类别不均衡明显的数据集上,OHEM比普通交叉熵能稳定提升1到2个点mIoU。
数据增强部分,我至少会加:
- 随机水平翻转,概率0.5。对街景来说左右翻转不改变语义结构,是一本万利的增强。
- 随机色彩抖动,包括亮度、对比度、饱和度调整。不同地区光照差异大,这是提前模拟全球化数据。
- 随机缩放和裁剪,前面提过,同时解决显存和尺度多样性两个问题。
- 极端情况下可以加随机擦除,模拟遮挡区域,增强模型鲁棒性。
还有一个常见的坑是BN层在训练和推理模式下的行为差异。如果训练中混用不同batch size,BN统计量会不稳定,我习惯固定输入尺寸和batch size,避免训练中期改batch size,否则后续几个epoch的验证指标会剧烈抖动。
损失函数还有一个进阶选项是Lovasz loss。它直接对mIoU做代理优化,虽然不能完全等价,但实践里对提升IoU指标很有效。如果追求贴合官方评测指标,可以试试交叉熵加Lovasz的组合,比如0.5权重配比。
3.3 评估指标与训练监控
分割任务的标准指标是mIoU,即平均交并比。对每个类别,算预测结果与真实标签的像素交集除以像素并集,再对所有类取平均。公式不复杂,但需要注意“并集”里的类别必须有足够样本,否则个别类别的IoU会因为少量像素的分歧剧烈波动。
Mapillary Vistas的官方评测通常排除部分未标注或ignore类,但本地验证时我建议保持和官方一致的做法,只在有效像素上计算IoU。
训练过程中的监控我一般看四样东西:
- 训练损失曲线。如果损失下降很慢,先检查学习率和数据加载是否正常。
- 验证集mIoU。每个epoch后跑一次,不用每个step都验证,节省时间。
- 各类IoU明细。重点关注小类别,如摩托车、自行车、护栏,这些最容易拖后腿。
- 可视化输出。每5到10个epoch随机抽几张验证图,把预测mask叠在原图上看。
很多时候mIoU数字看着还行,但可视化结果惨不忍睹。常见原因是大类别把指标拉高了,小类别的错误被平均数掩盖。可视化能让你看到模型真实的观感,这是纯数字给不了的。
我实际遇到过这种情况:某个模型整体mIoU是56,但行人类别IoU只有35,因为行人在总像素里占比太少,对整体mIoU的影响被稀释了。如果项目方要的是安全相关类别的质量,这类细节必须在评估报告里单独列出。
4. 踩坑记录与常见问题排查
4.1 类别不均衡与边界模糊问题
Mapillary Vistas的类别不均衡是这批数据最大的拦路虎。统计下来,训练集里道路和建筑两类像素合计占比超过四成,而摩托车、拖车、自行车这些目标合计占比可能连1%都不到。
如果让所有类别一起平等参与损失计算,模型训练早期会非常倾向于把不确定像素直接划为道路或建筑,因为这样损失下降最快。结果就是mIoU卡在40左右上不去,摩托车和行人类别低得没法看。
我自己的排查顺序是:
- 先看各类别像素占比分布,确认不均衡程度。
- 引入类别权重,权重反比于类别像素占比的平方根,减少强势类别对梯度的主导。
- 叠加OHEM,放大难例像素的梯度贡献。
- 效果还不够,就换用Lovasz loss或focal loss。
边界模糊是另一类问题。街景图里,建筑物和天空的交界、树冠边缘和背景的交界,像素本身就很模糊,人工标注也难以保持一致。模型在这些区域预测出一圈环状噪声非常正常。应对方法有两个:一是加边界感知loss,在损失计算里对边缘像素提高权重;二是在推理时对多尺度结果做融合,不同尺度预测平均后,边界稳定性明显提升。传统的CRF后处理在街景大数据量上速度太慢,现在已经不太常用。
4.2 显存不够时的实用策略
Mapillary Vistas原始图动辄超过2000×3000,一张8G显存的卡连一张图都放不下。我的优化思路按优先级排列:
减少batch size是最直接的做法,但要注意BN层统计量会受影响。batch size太小时,常规做法是改用group norm替代BN,但改动工程量稍大。如果batch size可以维持在2以上,通常影响可控。
降低输入尺寸最有效。从512×1024降到384×768,显存占用大约下降四成,指标一般只掉1到2个点。对大多数项目来说,这个取舍非常划算。
使用梯度累积。如果目标batch size是8,而每步只能跑2,那就累积4次反向传播梯度再统一更新。效果接近真正的大batch训练,但要注意batch size变大时学习率也要相应调整,否则收敛变慢。
用AMP混合精度训练。PyTorch里加autocast和GradScaler即可,显存能省30%左右,部分显卡上还能提速,主流显卡对FP16的支持都比较好,我还没遇到明显精度损失。
梯度检查点(gradient checkpointing)对Transformer类模型尤其好用。它不保存中间激活,反向传播时重新计算,代价是训练时间增加约20%,但能跑起更大的模型或更长的输入尺寸。
很多时候,组合使用“降低输入尺寸 + AMP + 梯度累积”三件套,就能在消费级显卡上跑完整个实验,不必非等服务器资源。
4.3 结果后处理与可视化交付
训练完模型后,不能只交一个权重文件。我通常会交付这些内容:
- 一份代码仓库,包含完整的数据预处理、训练、验证、推理脚本,保证对方能自己复现。
- 一份模型权重,配合训练时的配置文件,记录骨干网络、输入尺寸、epoch数、最优mIoU等关键参数。
- 一套结果可视化,包括原图、预测mask、类别图例、叠加图、抽帧演示。
- 一份评估报告,列出各类别IoU明细表和错误模式分析。
可视化配色也很讲究。语义分割的类别图例要保证区分度清晰:道路用灰色、建筑用浅棕、植被用绿色、天空用浅蓝、车辆用红色系、行人用黄色系。这种选择看似小事,但对方拿到结果后第一眼看的往往就是配色,如果一堆类别颜色混在一起,会立刻怀疑结果的可用性。
后处理时我还常用类别频次平滑。对成片的路面区域,如果预测结果中间出现一小块孤立的不合理区域,先检查是不是类别ID映射错误,排除后可以用形态学开闭运算消除小面积噪点。但别过度使用形态学操作,街景里的行人和自行车本身就是破碎小目标,过度平滑会把正确的小目标也抹掉。
5. 接代做需求的经验与流程管理
5.1 需求沟通:先搞清楚对方要什么
“接代做”这三个字,我理解是针对街景语义分割任务做定制化交付。做这类需求,最大的坑不在技术上,而在需求定义。
第一次沟通必须问清楚五个问题:
- 数据是否已备好。对方有没有数据集下载权限?如果没有,是否接受用其他公开数据替代?
- 模型部署环境。目标设备是服务器云GPU,还是本地工控机,甚至手机端?不同设备的模型大小和速度约束完全不同。
- 核心指标优先级。要最高精度,还是要能实时推理?要所有类别均衡,还是重点保证车、人、标志牌等安全相关类别?
- 交付物形态。是代码+权重+可视化报告,还是只要结果文件?对方能否自己重新训练?
- 时间窗口。几天、几周还是一个月?时间约束直接决定模型复杂度和实验轮数。
我碰到过一种情况:需求方只要“语义分割结果”,但没有说明标注类别体系和输出形式。我按66类直接开始做,结果对方只需要布尔类型的“可道路/非可道路”二分类。相当于把已经做的工作重做了一遍。后来我做了一个需求确认模板,逐条勾选,再没出现过类似返工。
5.2 交付流程与验收标准
确定需求后,我把项目拆成几个里程碑,并在每个节点和对方同步验收:
第一阶段:数据预处理完成,给出可视化样例。让对方确认标注格式、类别图例和效果预览。这一阶段出现误解的成本最低,改数据映射比改训练配置便宜得多。
第二阶段:基线模型训练完成,提交验证集mIoU和拼接结果图。此时如果指标离目标差距大,双方可以及时商议调整方案,不用等到最后再说。
第三阶段:最终模型和全套代码交付,包括README、依赖清单、训练和推理命令、配置文件说明。
验收标准要写进沟通记录。mIoU目标是多少、对哪些类别有硬性要求、推理速度上限、代码能否直接复现训练过程。把条件落到纸面,后面扯皮的概率会大大降低。
5.3 几个实际建议
最后分享几条我个人踩过的坑换来的经验:
- 尽量缩短交付周期。语义分割不是火箭科技,除非有特殊要求,不推荐超过3周的周期。周期越长,需求变动的可能性越大。
- 保留中间版本。每个阶段的核心代码、配置、权重都备份,标注好日期和指标。对方中途改需求时,你能快速回到某个版本继续开发,不用推倒重来。
- 环境依赖写清楚。PyTorch版本、CUDA版本、Python版本四者匹配关系弄混是最让人崩溃的事。交付时附一个requirements文件和启动命令。
- 不做超出能力范围的承诺。有些人为了接单什么都答应,最后交不出来。我的原则是先承认“这部分没把握”,再把拆解的难点列出来,看双方能不能一起调整方案。真诚反而更容易达成合作。
其实做到后面会越来越发现,这个方向的难点不在单点技术上,而在把一堆细节串联起来:数据格式的适配、模型与硬件的匹配、指标与可解释性的平衡。能把一条流程从头到尾跑通的人,对整套技术栈的理解已经超过平时只跑demo的人很多了。
我个人现在接同类需求前,一定会先问对方一句:你最关键的验收标准,到底是mIoU数字好看,还是这套结果能真正用在业务里?想清楚这个问题,项目就已经完成了一半。如果你正准备开跑Mapillary Vistas上的分割任务,数据预处理和模型基线这些步骤可以直接参考本文的方案,剩下的就看你的显卡和耐心了。