语义分割基础:给每个像素贴标签
本教程共 60 篇 · 第 38 篇 · 更新于 2026-08-17 · 约 4 分钟阅读
本节目标:弄清语义分割和分类、检测的区别,理解全卷积网络与上采样的思路,会用 torchvision 的预训练分割模型跑一次逐像素预测,并了解标签的形态与评估指标。
分类给整张图一个标签,检测给物体画框。语义分割(Semantic Segmentation)做得更彻底:给每个像素一个类别标签,输出一张「填色地图」。天空涂蓝、马路涂灰、行人涂红,画面里每一格都得有归属。自动驾驶、医学影像、人像抠图都离不开它。
具体点说:自动驾驶要分清路面、车道线和行人才能规划路线;医学影像里把肿瘤轮廓从 CT 片中勾出来,医生才看得清病灶边界;手机人像模式虚化背景,靠的也是逐像素分类。
把三种任务放一起对比就清楚了:分类输出一个数,检测输出若干框,分割输出的是一张和原图同样大小的标签图。注意语义分割不区分个体——两个挨着的行人会涂成同一种颜色。想分出「这是第一个行人、那是第二个」,那是实例分割的活。
顺带认识第三个词:全景分割(panoptic segmentation),把语义和实例结合起来——背景像素按类涂色,物体像素既分实例又分类。自动驾驶的感知系统常用这个设定,论文里经常出现,混个脸熟不亏。
分割也是标注成本最高的任务。分类给一张图打个标签几秒钟,检测画个框也要不了几秒,分割却要沿着物体边缘一点点描。好在医学影像、遥感这类场景里,轮廓本身就是核心信息,这份辛苦值得。读到这里你可能也发现了:分割的本质是一个超大的分类问题——把「给一张图分类」细化成「给上万个像素分别分类」。
全卷积网络
语义分割的经典思路叫全卷积网络(FCN,Fully Convolutional Network)。普通分类网络末段是几个全连接层,把空间信息全压扁成一个向量。FCN 把全连接层统统换成卷积层,网络从头到尾都在「看图」,输出天然是一张按比例缩小的特征图。
这里冒出一个新问题:卷积和池化让特征图越变越小。比如 224×224 的原图,过几层池化只剩 28×28。要拿到逐像素的标签图,就得把它放大回去。这个操作叫上采样(upsampling)。
最简单的上采样是插值:双线性插值和放大照片一个原理,按周边像素估算中间值。另一种是转置卷积(Transposed Convolution),自带可学习参数,可以理解为「反着跑的卷积」。还有第三种:像素混洗(pixel shuffle),把通道拆成空间位置,一步到位放大,现在的超分辨率模型常用它。跑个小例子感受下转置卷积:
import torch
import torch.nn as nn
x = torch.rand(1, 3, 8, 8)
# 卷积核 4×4、步长 2:尺寸翻倍
up = nn.ConvTranspose2d(3, 3, kernel_size=4, stride=2, padding=1)
print(up(x).shape) # torch.Size([1, 3, 16, 16])
不过光放大不够。浅层特征图分辨率高、细节多,深层特征图语义强、位置粗。把两者用跳跃连接(skip connection)拼起来,边放大边融合细节,就是分割网络的标准骨架。这套结构有个正式名字:编码器-解码器(encoder-decoder)。编码器负责看懂,解码器负责画出来。U-Net 把这条路走成了经典:左边编码器逐步下采样提取语义,右边解码器逐步上采样恢复分辨率,对称的结构像字母 U。它结构简单、小数据也能训,衍生版本至今活跃在各类竞赛里。
跑一次分割预测
torchvision 里有现成的分割模型,跑一次看看输出长什么样:
import torch
import torchvision
model = torchvision.models.segmentation.deeplabv3_resnet50(weights="DEFAULT")
model.eval()
x = torch.rand(1, 3, 520, 520)
with torch.no_grad():
out = model(x)["out"]
print(out.shape) # torch.Size([1, 21, 520, 520])
labels = out.argmax(dim=1) # 每个像素挑得分最高的类别
print(labels.shape) # torch.Size([1, 520, 520])
输出形状是 [批次, 21, 高, 宽],21 是类别数——COCO 的 20 类加背景。每个像素位置有 21 个分数,argmax(dim=1) 取最大的那个,就得到了一张标签图。注意输出的高宽和输入不一定相同——DeepLabv3 内部下采样后又上采样回来,一般会和原图对齐,但严谨起见,画图前先比对一下尺寸。想画出来看效果,把标签图映射成颜色即可:
import matplotlib.pyplot as plt
# 给 21 类各配一个颜色;类别数超过调色板长度时取模,避免越界
palette = torch.tensor([
[0, 0, 0], [255, 0, 0], [0, 255, 0], [0, 0, 255],
[255, 255, 0], [255, 0, 255], [0, 255, 255],
])
color_img = palette[labels[0] % len(palette)] # 形状 [H, W, 3]
plt.imshow(color_img)
plt.show()
Note分割模型的
model(x)返回一个字典,out是主输出。老教程里的pretrained=True写法已弃用,同样换成weights="DEFAULT"。
标签长什么样
训练自己的分割模型时,标签是一张「填色图」:每个像素的值就是类别编号。这和分类的标签完全不同——一张训练图配的不是一个数字,而是一整张图:
# 3×4 的标签图:0 是背景,1 是行人
mask = torch.tensor([
[0, 0, 1, 1],
[0, 1, 1, 1],
[0, 1, 1, 0],
])
Tip两个坑提前说。一是做数据增强时,图像和标签图必须做同样的裁剪、翻转,像素才会一一对应。二是标签图千万别归一化,它是一串类别编号,不是像素值,归一化会把语义全毁掉。
评估与模型选择
分割的评估指标是平均交并比(mIoU,mean Intersection over Union):对每一类算预测区域和真值区域的重叠比例,再取平均。数据不平衡时要多个心眼——天空占画面八成的话,全预测成天空也能拿高分,所以要分类别看 IoU。
模型谱系这样记:FCN 开山,U-Net 在医学影像里大名鼎鼎;DeepLab 系列用空洞卷积(dilated convolution)在不大幅缩小特征图的同时扩大视野。空洞卷积通俗讲就是让卷积核「撑开」,隔几个像素采一个点——感受野变大,细节却保留得更好。它像个带间隔的放大镜,看的地方更多,画面却不糊。
torchvision 内置了 fcn_resnet50、deeplabv3_resnet50 和轻量的 deeplabv3_mobilenet_v3_large。追求精度用 DeepLab 配 ResNet 主干,资源紧张就上 MobileNet 版。另外提醒一句:跑分割的显存开销不小,520×520 的输入在 CPU 上要等一会儿,初学可以先降到 256。
分割的下一个前沿是视频分割:给每一帧逐像素贴标签,还要保持前后帧的连续性,那是进阶玩家的乐园,入门先把静态图玩明白。