Day 15: 图像分割 (Image Segmentation)
摘要:如果说目标检测是给物体画框,那么图像分割就是把物体从背景中“抠”出来。它是计算机视觉中像素级别的分类任务。本文将带你从语义分割的开山之作 FCN 出发,深入 U-Net 和 DeepLab 细节,解析实例分割王者 Mask R-CNN,最后领略分割领域的 GPT——Segment Anything Model (SAM) 的风采。
1. 分割任务全家桶
在深入模型之前,我们需要先分清三个容易混淆的概念:
| 任务类型 | 英文 | 核心逻辑 | 例子 |
|---|---|---|---|
| 语义分割 | Semantic Segmentation | 只管类别,不管个体。所有“人”都是红色,所有“车”都是蓝色。 | 自动驾驶中区分路面、天空、车辆。 |
| 实例分割 | Instance Segmentation | 既管类别,也管个体。张三是红色,李四是绿色,虽然他们都是“人”。 | 机器人抓取特定物体。 |
| 全景分割 | Panoptic Segmentation | 语义 + 实例。背景(天空、草地)做语义分割,前景(人、车)做实例分割。 | 统一的场景理解。 |
2. 语义分割:从 FCN 到 DeepLab
2.1 FCN (Fully Convolutional Networks) - 全卷积网络
FCN 是深度学习做语义分割的开山鼻祖(CVPR 2015)。
- 核心思想:把分类网络(如 VGG)最后的全连接层(FC)丢掉,换成卷积层。
- 为什么?全连接层会丢失空间信息,且限制输入图片尺寸。全卷积网络可以接受任意尺寸输入,并输出一张“热力图”。
- 上采样 (Upsampling):卷积会让图片越来越小(下采样),分割需要输出原图大小。FCN 使用转置卷积 (Transposed Conv)把特征图放大回去。
2.2 U-Net - 医学影像的霸主
U-Net 的结构非常优美,像一个“U”字。
- 结构:左边是收缩路径(Encoder,提取特征),右边是扩张路径(Decoder,恢复尺寸)。
- 关键创新:跳跃连接 (Skip Connections)。
- 原理:深层特征语义强但位置模糊,浅层特征语义弱但边缘清晰。U-Net 把左边的浅层特征直接Concat (拼接)到右边对应的层。
- Concat vs Add:
- U-Net 用 Concat:意味着“我全都要”。左边的细节特征和右边的语义特征并排放在一起,让后续卷积层自己去选择用谁。这对于保留精细边缘至关重要。
- ResNet/FPN 用 Add:意味着“修正/增强”。在原有特征基础上叠加信息。FPN 使用 Add 主要是为了保持通道数一致以便共享检测头,且做多尺度特征融合。
2.3 DeepLab 系列 - 引入空洞卷积
Google 的 DeepLab 系列主要解决了两个问题:
- 下采样导致分辨率丢失:普通 CNN 也是一路池化,细节丢光了。
- 多尺度问题:物体有大有小。
解决方案:
- 空洞卷积 (Atrous/Dilated Convolution):
- 比喻:普通卷积像实心的九宫格印章,只能盖住一小块。空洞卷积是把九宫格拉开,中间留空。
- 作用:不池化也能看清大范围。在不降低分辨率(不缩小图片)的情况下,大幅扩大感受野。
- ASPP (Atrous Spatial Pyramid Pooling):
- 比喻:多倍镜同时拍摄。
- 原理:并行使用不同膨胀率(Rate=6, 12, 18)的空洞卷积去提取特征,然后融合。
- 效果:Rate=6 关注小物体(近景),Rate=18 关注大物体(远景),最后合在一起,大物体小物体一网打尽。
3. 实例分割:Mask R-CNN
Mask R-CNN (ICCV 2017 Best Paper) 是 Faster R-CNN 的完美进化。
- 思路:检测 + 分割。先找出框,再在框里做分割。
- 结构:Faster R-CNN + Mask 分支。
- Class Head:是什么?
- Box Head:在哪?
- Mask Head:像素掩码是什么?(新增分支)
关键技术:RoI Align
Faster R-CNN 使用 RoI Pooling 把框内的特征变成固定大小,这涉及到取整操作(Quantization)。
- 问题:对于分类,差几个像素没关系;但对于分割,几个像素的错位就是灾难(Mask 和原图对不齐)。
- 解决:RoI Align取消了取整,使用双线性插值来计算特征值,实现了像素级的对齐。
4. 分割大模型:SAM (Segment Anything)
2023年 Meta 发布的 SAM,被誉为计算机视觉领域的 GPT-3 时刻。
4.1 核心范式:Mask Prediction (非 NTP)
LLM 是 NTP (Next Token Prediction) 范式,像贪吃蛇一样逐词预测。
SAM 是 Mask Prediction 范式,类似于 DETR。它收到提示后,一次性并行输出完整的掩码矩阵,而不是逐像素生成。
4.2 架构解析:轻重分离
SAM 的设计兼顾了性能和效率,主要由三部分组成:
- Image Encoder (重型):
- 基于ViT-H (Vision Transformer)。
- 作用:把图片变成特征向量 (Embedding)。
- 特点:只算一次。不管后续如何交互,这张图的特征只算一遍,耗时较长但可复用。
- Prompt Encoder (轻量):
- 作用:把用户的各种提示变成向量。
- Mask Decoder (超轻量):
- 作用:结合图像特征和提示特征,毫秒级输出 Mask。这是实现实时交互的关键。
4.3 提示词 (Prompt) 的魔法:如何输入?
SAM 把物理世界的交互统统变成了数学向量:
- 点 (Point) & 框 (Box):
- 不是直接输坐标数字,而是通过位置编码 (Positional Encoding)。
- 类似于 Transformer 处理序列位置的方式,把(x,y)(x,y)(x,y)映射为高维向量,作为 “Token” 拼接到输入序列中。
- 掩码 (Mask):
- 如果上一轮预测了一个粗糙的 Mask,或者用户画了一笔,这个二维图像会经过一个CNN下采样,然后直接相加 (Add)到 Image Embedding 上,作为背景特征的一部分。
- 文本 (Text):
- 通过 CLIP 文本编码器变成向量。
4.4 输出与后处理
- 输出内容:SAM 输出的是Mask (掩码矩阵),即由 0 和 1 组成的黑白图。
- 如何抠图:需要进行后处理,用 Mask 与原图做点乘 (Element-wise Product),才能得到去除了背景的 RGBA 图像。
意义:SAM 解决了一个根本痛点——标注数据太贵。SAM 拥有强大的零样本 (Zero-shot) 能力,它可以作为通用的预处理工具,大大降低了下游任务的门槛。
5. 代码实践:PyTorch 实现简易 U-Net
importtorchimporttorch.nnasnnimporttorch.nn.functionalasFclassDoubleConv(nn.Module):"""(convolution => [BN] => ReLU) * 2"""def__init__(self,in_channels,out_channels):super().__init__()self.double_conv=nn.Sequential(nn.Conv2d(in_channels,out_channels,kernel_size=3,padding=1),nn.BatchNorm2d(out_channels),nn.ReLU(inplace=True),nn.Conv2d(out_channels,out_channels,kernel_size=3,padding=1),nn.BatchNorm2d(out_channels),nn.ReLU(inplace=True))defforward(self,x):returnself.double_conv(x)classUNet(nn.Module):def__init__(self,n_channels,n_classes):super(UNet,self).__init__()self.n_channels=n_channels self.n_classes=n_classes# Encoder (Downsampling)self.inc=DoubleConv(n_channels,64)self.down1=DoubleConv(64,128)self.down2=DoubleConv(128,256)self.down3=DoubleConv(256,512)self.down4=DoubleConv(512,1024)# MaxPoolself.pool=nn.MaxPool2d(2)# Decoder (Upsampling)self.up1=nn.ConvTranspose2d(1024,512,kernel_size=2,stride=2)self.conv_up1=DoubleConv(1024,512)# 512 from up + 512 from down3self.up2=nn.ConvTranspose2d(512,256,kernel_size=2,stride=2)self.conv_up2=DoubleConv(512,256)self.up3=nn.ConvTranspose2d(256,128,kernel_size=2,stride=2)self.conv_up3=DoubleConv(256,128)self.up4=nn.ConvTranspose2d(128,64,kernel_size=2,stride=2)self.conv_up4=DoubleConv(128,64)# Output layerself.outc=nn.Conv2d(64,n_classes,kernel_size=1)defforward(self,x):# Encoderx1=self.inc(x)x2=self.down1(self.pool(x1))x3=self.down2(self.pool(x2))x4=self.down3(self.pool(x3))x5=self.down4(self.pool(x4))# Decoder with Skip Connectionsx=self.up1(x5)# 实际使用中需要处理padding导致的尺寸不一致问题,这里简化处理假设尺寸匹配# cat(x, x4)x=torch.cat([x4,x],dim=1)x=self.conv_up1(x)x=self.up2(x)x=torch.cat([x3,x],dim=1)x=self.conv_up2(x)x=self.up3(x)x=torch.cat([x2,x],dim=1)x=self.conv_up3(x)x=self.up4(x)x=torch.cat([x1,x],dim=1)x=self.conv_up4(x)logits=self.outc(x)returnlogits# 测试模型if__name__=="__main__":model=UNet(n_channels=3,n_classes=10)# 假设输入图片大小为 160x160 (必须是16的倍数,否则concat时尺寸会不匹配)x=torch.randn(1,3,160,160)y=model(x)print(f"Input shape:{x.shape}")print(f"Output shape:{y.shape}")# Should be [1, 10, 160, 160]6. 总结与思考
- 语义分割:FCN 打开了大门,DeepLab 用空洞卷积(不缩小看清全图)和 ASPP(多倍镜看细节与轮廓)解决了多尺度问题,U-Net 用 Concat 跳跃连接保留了极致的边缘细节。
- 实例分割:Mask R-CNN 在检测框里做精细分割,RoI Align 解决了像素对齐的痛点。
- 大模型时代:SAM 引入了 Prompt 机制,将点/框/文映射为向量,配合轻量级 Mask Decoder,实现了“指哪打哪”的通用分割能力。
思考:为什么 Feature Pyramid Network (FPN) 用 Add 而 U-Net 用 Concat?
- FPN (检测):追求多尺度特征统一。P3, P4, P5 需要有相同的通道数(如256)以便共享检测头,Add 可以保持通道数不变,且类似于 ResNet 起到特征增强的作用。
- U-Net (分割):追求像素级还原。Concat 可以最大程度保留浅层的空间信息(坐标、边缘),这对像素分类任务至关重要。