1. 图像分割到底在解决什么问题
图像分割这个词,刚接触计算机视觉的朋友可能会觉得它和图像分类、目标检测差不多,都是让机器“看懂”图片。但实际用过的人都知道,这三者的颗粒度完全不在一个量级。分类是告诉你“这张图里有猫”,检测是告诉你“猫在左上角那个框里”,而分割要回答的是——“图片里每一个像素,到底属于猫、属于背景、还是属于旁边的沙发”。
我第一次接触分割是在做一个广告牌识别的小项目。当时想得很简单,检测框把广告牌框出来不就行了?结果客户说不行,广告牌是斜的,框里一大半是天空和树,他们要的是精确到边缘的轮廓。那个项目让我彻底理解了分割的价值:它输出的是像素级的类别地图,而不是一个粗糙的矩形框。
从技术上讲,图像分割分为三大类。语义分割把每个像素归到某个类别,但不区分同一类别的不同个体,比如两张桌子挨在一起,语义分割会把它们标成同一块“桌子”区域。实例分割则要区分出“桌子A”和“桌子B”,每个个体单独成块。还有全景分割,把语义和实例合在一起,既分背景也分个体。日常工业项目里,语义分割的需求量最大,像遥感影像地物分类、医学影像器官提取、道路场景理解,基本都是语义分割的范畴。
那为什么标题里提到的FCN、U-Net、SegNet、Deeplab、SegFormer这几个算法这么重要?因为它们代表了语义分割从“能跑通”到“跑得准”再到“跑得快”的三个阶段。FCN是开山之作,第一次用全卷积网络端到端做分割;U-Net把编码器-解码器结构玩出了花,在医学图像上封神;SegNet用池化索引做上采样,省显存;Deeplab系列靠空洞卷积和多尺度融合把精度推到新高度;SegFormer则用Transformer架构把效率和精度同时拉满。把这五个算法吃透,基本就覆盖了语义分割的主流技术路线。
这篇文章适合谁看?如果你已经会用PyTorch搭简单的CNN分类网络,想往分割方向深入,那这篇就是为你写的。如果你正在做地毯图像分割系统、广告牌图像分割系统这类实际项目,需要选型和技术细节,也能直接抄作业。我会从每个算法的核心设计讲起,配上关键代码和参数计算,再分享一些踩过的坑。
2. 五个核心算法的设计思路拆解
2.1 FCN:第一次让分割变成端到端任务
在FCN出现之前,做分割的套路是:用CNN提取特征,然后对每个像素周围取一个patch,用分类器判断这个像素的类别。这种方法有多慢?一张500x500的图,25万个像素,每个都要单独跑一次CNN,实际项目里根本没法用。而且patch之间大量重叠,计算冗余极其严重。
FCN的核心贡献就一句话:把CNN最后面的全连接层换成卷积层。全连接层要求输入尺寸固定,换成卷积层之后,网络可以接受任意尺寸的输入,输出也不再是一个类别向量,而是一张二维的特征图。这张特征图经过上采样恢复到原图尺寸,每个位置的值就代表该像素属于各类别的得分。
但直接上采样有个问题:深层特征图分辨率太低,丢失了细节。FCN的解决方案是跳跃连接,把浅层的高分辨率特征和深层的低分辨率特征融合。具体来说,FCN-8s把pool4和pool3的输出分别上采样后与最后的特征图相加,这样既保留了语义信息,又恢复了边缘细节。
我实测下来,FCN-8s在PASCAL VOC上能到67%左右的mIoU,放在今天不算高,但它的设计思想影响深远。全卷积、跳跃连接、端到端训练这三个概念,后面几乎所有分割网络都在用。
2.2 U-Net:医学图像分割的标配
U-Net的结构像一个大写的U,左边是编码器,右边是解码器,中间用跳跃连接把对应层的特征拼起来。这个设计其实和FCN的跳跃连接思路一致,但U-Net做得更彻底:每一层编码器的特征都直接拼到对应解码器层,而不是像FCN那样只融合几层。
为什么U-Net在医学图像上这么能打?因为医学图像有两个特点:一是样本少,二是边界模糊。U-Net的跳跃连接把浅层的高分辨率纹理信息完整地传到了解码端,这对勾勒器官边界至关重要。而且它的结构非常对称,参数量可控,在小数据集上不容易过拟合。
U-Net的编码器每层是两个3x3卷积加ReLU,然后2x2最大池化下采样。解码器每层是先上采样,然后和编码器对应层拼接,再两个3x3卷积。最后一层1x1卷积把通道数映射到类别数。这个结构我手写过不下十遍,每次做新项目都会先拿U-Net跑个baseline。
有个细节值得注意:U-Net原论文里上采样用的是转置卷积,但后来很多人改成双线性插值加卷积,效果差不多但显存占用更小。我在显存吃紧的时候会优先用后者。
2.3 SegNet:用池化索引省显存的巧思
SegNet和U-Net长得很像,也是编码器-解码器结构,但它的上采样方式很特别。U-Net用转置卷积或者插值,SegNet用的是池化索引。什么意思?编码器做最大池化的时候,记录下每个2x2区域里最大值的位置。解码器上采样时,把值直接填回这些记录的位置,其他位置补零。
这样做的好处是什么?不需要学习上采样的参数,显存占用和计算量都更小。在嵌入式设备或者显存有限的场景下,这个设计很实用。但缺点也明显:补零会让特征图变得稀疏,后续卷积需要额外操作来平滑。而且池化索引只保留了最大值的位置信息,其他三个位置的信息完全丢失了。
我做过一个对比实验,同样的编码器下,SegNet的显存占用比U-Net少大概30%,但mIoU会低1到2个点。如果你的场景对精度要求不是极致,但显存卡得很死,SegNet是个不错的选择。
2.4 Deeplab系列:空洞卷积和多尺度融合的集大成者
Deeplab系列从v1到v3+,核心一直在解决一个问题:如何在不降低分辨率的情况下扩大感受野。传统做法是池化下采样,但下采样会丢失空间信息。Deeplab的答案是空洞卷积,在卷积核里插入空洞,让同样的参数量覆盖更大的区域。
Deeplab v1引入了空洞卷积和条件随机场后处理。v2提出了ASPP,用不同空洞率的卷积并行提取多尺度特征。v3改进了ASPP,加入了全局平均池化和批量归一化。v3+则加上了编码器-解码器结构,把浅层特征也利用起来。
ASPP是Deeplab的灵魂。它用1x1卷积、3x3空洞率6、3x3空洞率12、3x3空洞率18四个分支并行,最后把输出拼起来。这样网络能同时看到近处和远处的上下文。我在地毯图像分割系统里用过Deeplab v3+,地毯的花纹尺度变化很大,ASPP的多尺度能力确实帮了大忙。
2.5 SegFormer:Transformer在分割上的正确打开方式
SegFormer是2021年的工作,它把Transformer引入语义分割,但做了一系列针对性的简化。它的编码器是分层的Transformer,输出多尺度特征;解码器是一个极轻量的MLP头,把多尺度特征统一到同一维度后融合。
SegFormer最让我惊艳的是它的效率。在Cityscapes数据集上,SegFormer-B0只有3.8M参数,却能跑到76%以上的mIoU,推理速度比Deeplab v3+快好几倍。它的秘诀在于:编码器用无位置编码的注意力机制,解码器用MLP代替复杂的上采样。位置编码在分割任务里其实会带来麻烦,因为测试时图像尺寸和训练时不一致,位置编码需要插值,容易掉点。SegFormer直接去掉了位置编码,靠卷积来隐式编码位置信息。
我在广告牌图像分割系统里对比过SegFormer和Deeplab v3+,同样的数据量下,SegFormer收敛更快,最终精度也更高。但要注意,SegFormer对数据增强比较敏感,需要配合较强的增强策略才能发挥全部实力。
3. 从零搭建分割项目的实操流程
3.1 数据准备与标注格式转换
做分割项目,数据永远是第一位的。标注格式常见的有三种:PNG掩码图、COCO JSON、VOC XML。PNG掩码图最直观,每个像素的值就是类别ID,0是背景,1是类别1,以此类推。COCO和VOC是通用格式,但解析起来麻烦一些。
我一般建议团队直接用PNG掩码图,因为可视化方便,调试的时候一眼就能看出标注对不对。如果拿到的是COCO格式,可以用pycocotools转成掩码图。转换的时候要注意类别ID的映射,COCO的类别ID不连续,需要重新映射成0到N-1。
数据增强对分割特别重要。常用的有随机缩放、随机裁剪、随机翻转、颜色抖动。但要注意,几何变换必须同时作用于图像和掩码,而且掩码要用最近邻插值,不能用双线性,否则会出现不存在的类别值。我见过有人用双线性插值处理掩码,结果训练时loss一直不降,排查了半天才发现是掩码被插值成了小数。
3.2 损失函数选择与类别不平衡处理
分割任务最常用的损失是交叉熵,但直接用它有个问题:背景像素通常占绝大多数,模型会倾向于把所有像素预测成背景。解决办法有两种:一是用带权重的交叉熵,给少数类更高的权重;二是用Dice Loss或Focal Loss。
Dice Loss直接优化预测区域和真实区域的重叠度,对类别不平衡不敏感。但Dice Loss的梯度在预测和真实完全重合时会变得不稳定,所以实践中常用交叉熵和Dice Loss的加权和。我一般设交叉熵权重0.5,Dice权重0.5,效果比较稳。
还有一个技巧是在线难例挖掘,把loss最高的那部分像素挑出来重点训练。这个在广告牌分割里特别有用,因为广告牌的边缘像素很容易分错,OHEM能让模型聚焦在这些难例上。
3.3 训练策略与学习率调度
分割网络的训练通常用SGD或AdamW。SGD配合动量0.9和权重衰减1e-4是经典配置,AdamW在Transformer类模型上表现更好。学习率方面,多项式衰减是分割任务的标准做法,初始学习率设0.01或0.001,衰减幂次0.9。
Batch size尽量往大了设,因为分割对batch内的统计信息比较依赖。如果显存不够,可以用梯度累积,比如实际batch size 16,但每次只跑4张图,累积4次再更新。这样等效于大batch,但显存占用小。
训练轮数方面,小数据集(几千张)一般跑100到200个epoch,大数据集(几万张)跑50到80个epoch就够了。我习惯每10个epoch验证一次,保存验证集mIoU最高的模型。早停策略也很重要,如果连续20个epoch验证集指标不升,就停掉,避免过拟合。
3.4 推理部署与性能优化
训练完的模型要部署到实际系统里,推理速度就是关键了。几个常用的优化手段:模型剪枝去掉冗余通道,量化把FP32转成FP16或INT8,TensorRT做图优化和算子融合。
我在地毯图像分割系统里做过测试,Deeplab v3+在RTX 3060上原始推理速度是25FPS,经过FP16量化后到45FPS,再用TensorRT优化后到70FPS,完全满足产线实时性要求。但量化会掉一点精度,一般掉0.5到1个mIoU,需要根据业务容忍度权衡。
还有一个容易忽略的点是输入分辨率。训练时用512x512,推理时如果直接上1024x1024,精度可能会掉,因为感受野和统计分布变了。稳妥的做法是推理分辨率尽量和训练一致,或者用多尺度测试再融合结果。
4. 常见问题与排查技巧实录
4.1 训练loss不下降或震荡严重
这是最常见的问题,原因通常有这几个:学习率太大、数据标注有问题、损失函数权重不合理。排查顺序建议从数据开始,可视化几张训练样本和对应的掩码,看看掩码是不是全黑或者类别错位。我遇到过标注工具导出的掩码颜色通道反了,导致模型一直在学错误的目标。
如果数据没问题,就把学习率降一个数量级试试。分割网络对学习率比较敏感,尤其是用Adam的时候,初始学习率超过1e-3很容易震荡。另外检查一下损失函数,如果Dice Loss权重过高,训练初期梯度会很不稳定。
4.2 验证集精度远低于训练集
典型的过拟合。解决办法:加数据增强、加Dropout、加权重衰减、减少模型参数量。分割任务里,随机裁剪是最有效的增强手段,因为它同时增加了样本多样性和尺度多样性。我一般会把随机裁剪的比例设成0.5到1.0之间随机,让模型看到不同尺度的目标。
还有一个可能是训练集和验证集分布不一致。比如训练集都是白天拍的,验证集有夜景,那精度肯定掉。这种情况需要做域适应,或者干脆在验证集分布上也采一些数据加入训练。
4.3 边缘分割不准确
边缘不准是分割的顽疾。原因主要是下采样丢失了空间信息。解决办法:用跳跃连接把浅层特征传上来,或者用CRF后处理细化边缘。CRF的计算量比较大,可以用快速版的高斯滤波近似。
另一个技巧是多尺度测试,把图像缩放到不同尺度分别推理,再把结果平均。这样边缘会更平滑,但推理时间成倍增加。实际项目里,如果边缘要求特别高,我会在损失函数里加一个边界损失,专门惩罚边缘区域的错误。
4.4 显存不够用
显存不够的时候,优先降batch size,然后降输入分辨率。如果还不够,可以试试混合精度训练,用FP16存激活值,显存占用能降30%到40%。PyTorch的amp模块开箱即用,几行代码就能搞定。
模型层面,可以把编码器换成轻量级的,比如MobileNetV2或EfficientNet-B0。SegFormer-B0本身就很轻,3.8M参数,显存占用比ResNet-50 backbone的Deeplab小很多。如果还不行,就用梯度检查点,用计算时间换显存空间。
4.5 类别不平衡导致小目标被忽略
小目标分割是难点。除了前面说的损失函数加权,还可以用多尺度训练,让小目标在某些尺度下变大。另外,采样策略也很重要,构建batch的时候尽量让每个batch里都包含小目标的样本。
我在地毯图像分割系统里遇到过花纹细节丢失的问题,后来在损失函数里加了一个细节损失,对高频区域加大权重,效果很明显。具体做法是用拉普拉斯算子提取图像的边缘,在边缘区域给更高的loss权重。
5. 算法选型与场景适配建议
5.1 不同场景下的算法推荐
| 场景 | 推荐算法 | 理由 |
|---|---|---|
| 医学影像分割 | U-Net / U-Net++ | 小样本友好,边界精细 |
| 遥感地物分类 | Deeplab v3+ / SegFormer | 多尺度能力强,精度高 |
| 自动驾驶道路分割 | SegFormer / Deeplab v3+ | 实时性和精度平衡 |
| 嵌入式设备部署 | SegNet / SegFormer-B0 | 显存占用小,推理快 |
| 广告牌/地毯分割 | Deeplab v3+ / SegFormer | 纹理复杂,需要多尺度 |
| 快速原型验证 | FCN-8s / U-Net | 结构简单,容易调试 |
选型的时候不要盲目追新。SegFormer虽然强,但如果你的数据集只有几百张图,U-Net可能更稳。Deeplab v3+精度高,但推理速度慢,实时场景要慎重。先跑通baseline,再根据瓶颈换模型,这是我做项目的一贯原则。
5.2 从FCN到SegFormer的技术演进逻辑
回头看这五个算法,技术演进的脉络很清晰。FCN解决了“能不能端到端”的问题,U-Net和SegNet解决了“怎么更好地恢复分辨率”的问题,Deeplab解决了“怎么扩大感受野”的问题,SegFormer解决了“怎么用Transformer又不用太贵”的问题。
每一步演进都是在精度、速度、显存这三个维度上做权衡。没有哪个算法是全面碾压的,关键看你的场景最在意什么。我在实际项目里经常混用,比如用SegFormer的编码器配一个轻量解码器,或者用Deeplab的ASPP模块加到U-Net上。理解每个模块的设计意图,比死记网络结构重要得多。
5.3 迁移学习与预训练权重使用
分割数据集通常不大,从头训练很容易过拟合。用ImageNet预训练的编码器是标准操作,能带来好几个点的mIoU提升。PyTorch的torchvision里有很多预训练backbone,直接拿来用就行。
如果做的是特定领域,比如医学或者遥感,ImageNet预训练可能帮助有限,因为域差异太大。这时候可以用自监督预训练,比如MoCo v3或者MAE,在领域数据上先预训练编码器,再微调分割头。我试过在遥感数据上用MAE预训练,比ImageNet预训练高了3个点。
还有一个技巧是冻结编码器前几层,只训练后面的层。这样训练更稳定,收敛更快,尤其适合小数据集。等loss降下来了再解冻全部层做微调。
6. 我踩过的坑和实操心得
做分割这几年,踩的坑比写的代码还多。说几个印象深刻的。
第一个坑是忽略数据标注质量。有次做广告牌分割,标注团队把广告牌的阴影也标成了广告牌,模型学出来之后,推理时把地面阴影也预测成广告牌。后来重新定义了标注规范,明确阴影不算广告牌区域,精度才上来。标注规范一定要在标注开始前就定死,中途改代价极大。
第二个坑是盲目上大模型。有次接了个地毯花纹分割的项目,上来就用Deeplab v3+配ResNet-101,结果训练慢、推理慢,精度也没比U-Net高多少。后来换成U-Net配EfficientNet-B3,精度差不多,速度快了三倍。模型不是越大越好,匹配数据量和场景才是关键。
第三个坑是忽略后处理。分割网络的原始输出往往边缘毛糙,加一个简单的形态学操作或者连通域过滤就能提升视觉效果。比如去掉面积小于50像素的连通域,或者用开运算平滑边缘。这些后处理不增加训练成本,但用户感知很明显。
第四个坑是测试时预处理不一致。训练时用了归一化,推理时忘了,结果精度暴跌。这种低级错误我犯过不止一次。后来养成了习惯,把预处理和后处理都封装成函数,训练和推理共用同一套代码。
最后分享一个实用技巧:用TensorBoard或WandB记录训练过程,把每个epoch的预测结果可视化出来。这样能直观看到模型在哪些类别上进步,哪些类别一直分不好。我经常通过看可视化结果发现数据问题,比如某个类别的样本太少,或者标注有系统性错误。
分割这个方向,算法更新很快,但核心思想变化不大。把FCN、U-Net、SegNet、Deeplab、SegFormer这五个吃透,再新的算法也能快速上手。关键是多动手,多踩坑,多总结。