第五篇:图像分割与实例分割#

从像素级理解到万物分割,掌握图像分割的完整技术栈

篇章概览#

图像分割是计算机视觉的核心任务之一,它不仅要识别"哪里有物体"(目标检测),还要精确描绘"物体的每一个像素"。本篇将系统学习:

  • 语义分割:为每个像素分配类别标签
  • 实例分割:区分同一类别的不同个体
  • Segment Anything:零样本分割的革命性突破

为什么要学习图像分割?#

1. 更精细的视觉理解#

目标检测:这里有一辆车 [矩形框]
语义分割:这些像素是车 [像素级mask]
实例分割:这是第1辆车,那是第2辆车 [区分个体]

2. 广泛的应用场景#

  • 医学影像:肿瘤分割、器官分割
  • 自动驾驶:道路分割、车道线检测
  • 遥感分析:土地利用分类
  • 图像编辑:抠图、背景替换
  • 工业检测:缺陷分割

3. 技术发展迅速#

  • 从FCN到U-Net:编码器-解码器架构
  • 从Mask R-CNN到YOLACT:实时实例分割
  • 从SAM到SAM 2:零样本视频分割

篇章结构#

第11章:语义分割#

语义分割为每个像素分配类别标签,不区分同类物体的个体。

核心内容

  • 11.1 FCN:全卷积网络的开创性工作
  • 11.2 U-Net:医学图像分割的经典架构
  • 11.3 DeepLab系列:空洞卷积与ASPP
  • 11.4 实战:医学图像分割项目

代码实践

  • U-Net完整实现与训练
  • 医学影像数据处理
  • 分割评估指标(IoU、Dice)

第12章:实例分割#

实例分割不仅识别像素类别,还要区分同类物体的不同个体。

核心内容

  • 12.1 Mask R-CNN:两阶段实例分割
  • 12.2 YOLACT:实时实例分割
  • 12.3 YOLOv8-Seg:YOLO的分割版本
  • 12.4 实战:COCO实例分割

代码实践

  • YOLOv8-Seg训练与推理
  • 实例分割后处理
  • 可视化mask输出

第13章:Segment Anything (SAM)#

Meta的SAM模型开启了"万物分割"的新时代,支持零样本分割。

核心内容

  • 13.1 SAM模型架构详解
  • 13.2 Prompt Engineering for SAM
  • 13.3 SAM 2:视频分割能力
  • 13.4 实战:零样本分割应用

代码实践

  • SAM点击式分割
  • SAM边界框分割
  • SAM 2视频对象分割

分割任务对比#

任务类型定义输出应用场景代表模型
语义分割像素级分类类别掩码场景理解、医学影像FCN, U-Net, DeepLab
实例分割区分个体实例掩码自动驾驶、工业检测Mask R-CNN, YOLOv8-Seg
全景分割语义+实例统一掩码完整场景理解Panoptic FPN
交互式分割用户引导交互掩码图像编辑、标注工具SAM, SAM 2

评估指标#

1. IoU (Intersection over Union)#

IoU = 交集 / 并集 = (预测 ∩ 真实) / (预测 ∪ 真实)

优点:直观,易于理解 缺点:对类别不平衡敏感

2. Dice Coefficient (F1 Score)#

Dice = 2 × 交集 / (预测 + 真实) = 2 × |A ∩ B| / (|A| + |B|)

优点:对小目标更友好 应用:医学图像分割常用

3. mIoU (mean IoU)#

mIoU = (1/N) × Σ IoU_i

对所有类别的IoU取平均,评估整体性能。

4. Pixel Accuracy#

PA = 正确分类的像素数 / 总像素数

缺点:对类别不平衡非常敏感

技术演进路线#

2015    FCN           全卷积网络,开创像素级预测
2015    U-Net         编码器-解码器,医学图像分割经典
2017    DeepLab v3    空洞卷积 + ASPP
2017    Mask R-CNN    实例分割里程碑
2019    YOLACT        实时实例分割
2023    YOLOv8-Seg    YOLO统一框架
2023    SAM           零样本分割,Prompt驱动
2024    SAM 2         扩展到视频分割

学习路径建议#

入门阶段#

  1. 理解语义分割与实例分割的区别
  2. 掌握FCN和U-Net的核心思想
  3. 实践医学图像分割项目

进阶阶段#

  1. 学习Mask R-CNN的两阶段设计
  2. 掌握YOLOv8-Seg的实时分割
  3. 理解空洞卷积的作用

高级阶段#

  1. 深入SAM的架构和Prompt机制
  2. 探索SAM 2的视频分割能力
  3. 应用于实际项目

实战项目#

1. 医学图像分割#

  • 数据集:Medical Segmentation Decathlon
  • 任务:肺部CT分割
  • 模型:U-Net
  • 指标:Dice Coefficient

2. COCO实例分割#

  • 数据集:COCO 2017
  • 任务:80类物体实例分割
  • 模型:YOLOv8-Seg
  • 指标:mAP (box + mask)

3. 零样本分割应用#

  • 任务:自定义物体分割
  • 模型:SAM
  • 交互:点击、框选、文本提示

代码示例#

语义分割#

import torch
from models import UNet

# 加载U-Net模型
model = UNet(in_channels=1, num_classes=2)
model.load_state_dict(torch.load('unet.pth'))

# 推理
image = load_medical_image('ct_scan.nii')
with torch.no_grad():
    mask = model(image)
    prediction = torch.argmax(mask, dim=1)

实例分割#

from ultralytics import YOLO

# 加载YOLOv8-Seg
model = YOLO('yolo11n-seg.pt')

# 推理
results = model('street.jpg')
masks = results[0].masks.data  # 实例掩码
boxes = results[0].boxes.data  # 边界框

零样本分割#

from transformers import SamModel, SamProcessor

# 加载SAM
model = SamModel.from_pretrained("facebook/sam-vit-huge")
processor = SamProcessor.from_pretrained("facebook/sam-vit-huge")

# 点击式分割
input_points = [[[450, 600]]]  # 点击坐标
inputs = processor(image, input_points=input_points, return_tensors="pt")
outputs = model(**inputs)
masks = processor.post_process_masks(outputs.pred_masks)

工具与资源#

开源框架#

  • Ultralytics:YOLOv8-Seg实现
  • MMSegmentation:丰富的分割模型库
  • Segmentation Models PyTorch:预训练分割模型

数据集#

  • COCO:实例分割标准数据集
  • Pascal VOC:语义分割经典数据集
  • Cityscapes:自动驾驶场景分割
  • ADE20K:场景解析数据集
  • Medical Decathlon:医学图像分割

标注工具#

  • LabelMe:通用分割标注
  • CVAT:支持实例分割
  • SAM Demo:基于SAM的快速标注

学习目标#

学完本篇后,你将能够:

  1. 理解分割任务

    • 区分语义分割、实例分割、全景分割
    • 掌握评估指标的计算和意义
  2. 掌握核心模型

    • 实现U-Net从零开始
    • 使用YOLOv8-Seg进行实例分割
    • 应用SAM进行零样本分割
  3. 完成实战项目

    • 医学图像分割
    • COCO实例分割
    • 自定义分割应用
  4. 解决实际问题

    • 小目标分割优化
    • 类别不平衡处理
    • 实时性能优化

下一步#

准备好了吗?让我们从第11章开始,深入学习语义分割的核心技术!


本篇特色

  • 从经典到前沿的完整技术栈
  • 医学图像分割实战项目
  • SAM零样本分割深度解析
  • 完整可运行的代码示例

第11章:语义分割#

从FCN到DeepLab,掌握像素级分类的核心技术

本章概览#

语义分割(Semantic Segmentation)是计算机视觉的基础任务之一,目标是为图像中的每个像素分配一个类别标签。与目标检测不同,语义分割不仅要找到物体,还要精确描绘其边界。

核心内容

  • FCN如何开创全卷积网络范式
  • U-Net为何成为医学图像分割的金标准
  • DeepLab的空洞卷积如何扩大感受野
  • 实战医学图像分割项目

什么是语义分割?#

任务定义#

给定输入图像 I ∈ R^(H×W×3),输出分割掩码 S ∈ {1,2,...,C}^(H×W),其中C是类别数。

示例

输入:街景图像 (512×512×3)
输出:
  - 像素(100, 200) → 类别2 (人)
  - 像素(300, 150) → 类别7 (汽车)
  - 像素(450, 400) → 类别0 (背景)

与其他任务的区别#

任务输出特点示例
图像分类单一标签整图级别“这是一只猫”
目标检测边界框+类别物体级别“这里有2只猫”
语义分割像素级类别像素级别“这些像素是猫”
实例分割像素级实例区分个体“第1只猫、第2只猫”

应用场景#

  1. 医学影像分析

    • 器官分割(肝脏、肺部)
    • 病灶检测(肿瘤、病变)
    • 细胞分割
  2. 自动驾驶

    • 道路分割
    • 车道线检测
    • 障碍物识别
  3. 遥感图像分析

    • 土地利用分类
    • 建筑物提取
    • 森林监测
  4. 图像编辑

    • 人像抠图
    • 背景替换
    • 风格迁移

11.1 FCN:全卷积网络#

论文:Fully Convolutional Networks for Semantic Segmentation (CVPR 2015) 作者:Jonathan Long, Evan Shelhamer, Trevor Darrell (UC Berkeley)

11.1.1 核心思想#

FCN的革命性贡献:将分类网络改造为全卷积网络,实现端到端的像素级预测

传统分类网络的问题

输入图像 (224×224×3)
卷积层 (提取特征)
全连接层 (分类)
输出:单一类别标签

全连接层有两个问题:

  1. 固定输入尺寸:必须是224×224
  2. 丢失空间信息:输出是1D向量

FCN的解决方案

输入图像 (任意尺寸 H×W×3)
全卷积层 (无全连接层)
上采样 (恢复分辨率)
输出:像素级预测 (H×W×C)

11.1.2 架构设计#

1. 全卷积化#

将AlexNet/VGG的全连接层替换为1×1卷积:

# 传统分类网络
fc6 = nn.Linear(512 * 7 * 7, 4096)  # 展平后全连接

# FCN改造
fc6 = nn.Conv2d(512, 4096, kernel_size=7)  # 7×7卷积
fc7 = nn.Conv2d(4096, 4096, kernel_size=1)  # 1×1卷积
score = nn.Conv2d(4096, num_classes, kernel_size=1)  # 分类层

优势

  • 接受任意尺寸输入
  • 保留空间信息
  • 输出热图(heatmap)

2. 上采样(Upsampling)#

经过卷积和池化后,特征图尺寸缩小,需要恢复到原始分辨率。

方法1:双线性插值上采样

upsampled = F.interpolate(x, scale_factor=2, mode='bilinear')

方法2:转置卷积(Transposed Convolution)

# 也称为反卷积(Deconvolution)
upsample = nn.ConvTranspose2d(
    in_channels=512,
    out_channels=num_classes,
    kernel_size=4,
    stride=2,
    padding=1
)

转置卷积原理

标准卷积:输入4×4 → (kernel=3, stride=2) → 输出2×2 转置卷积:输入2×2 → (kernel=3, stride=2) → 输出4×4

3. 跳跃连接(Skip Connections)#

问题:直接32倍上采样会丢失细节

解决方案:融合不同层的特征

输入图像 (H×W)
conv1 → pool1 (H/2×W/2)  ←─┐
    ↓                      │ Skip
conv2 → pool2 (H/4×W/4)  ←─┼─┐
    ↓                      │ │ Skip
conv3 → pool3 (H/8×W/8)  ←─┼─┼─┐
    ↓                      │ │ │
[卷积层]                   │ │ │
    ↓                      │ │ │
上采样2倍 + 融合pool3 ──────┘ │ │
    ↓                          │ │
上采样2倍 + 融合pool2 ──────────┘ │
    ↓                            │
上采样2倍 + 融合pool1 ────────────┘
输出 (H×W×C)

三种变体

模型上采样倍数跳跃连接性能
FCN-32s32倍粗糙
FCN-16s16倍pool4改善
FCN-8s8倍pool4 + pool3最佳

4. FCN-8s架构代码#

import torch
import torch.nn as nn
import torch.nn.functional as F

class FCN8s(nn.Module):
    def __init__(self, num_classes=21):
        super(FCN8s, self).__init__()

        # VGG16的卷积层
        # Block 1
        self.conv1_1 = nn.Conv2d(3, 64, 3, padding=1)
        self.relu1_1 = nn.ReLU(inplace=True)
        self.conv1_2 = nn.Conv2d(64, 64, 3, padding=1)
        self.relu1_2 = nn.ReLU(inplace=True)
        self.pool1 = nn.MaxPool2d(2, stride=2, ceil_mode=True)

        # Block 2
        self.conv2_1 = nn.Conv2d(64, 128, 3, padding=1)
        self.relu2_1 = nn.ReLU(inplace=True)
        self.conv2_2 = nn.Conv2d(128, 128, 3, padding=1)
        self.relu2_2 = nn.ReLU(inplace=True)
        self.pool2 = nn.MaxPool2d(2, stride=2, ceil_mode=True)

        # Block 3
        self.conv3_1 = nn.Conv2d(128, 256, 3, padding=1)
        self.relu3_1 = nn.ReLU(inplace=True)
        self.conv3_2 = nn.Conv2d(256, 256, 3, padding=1)
        self.relu3_2 = nn.ReLU(inplace=True)
        self.conv3_3 = nn.Conv2d(256, 256, 3, padding=1)
        self.relu3_3 = nn.ReLU(inplace=True)
        self.pool3 = nn.MaxPool2d(2, stride=2, ceil_mode=True)

        # Block 4
        self.conv4_1 = nn.Conv2d(256, 512, 3, padding=1)
        self.relu4_1 = nn.ReLU(inplace=True)
        self.conv4_2 = nn.Conv2d(512, 512, 3, padding=1)
        self.relu4_2 = nn.ReLU(inplace=True)
        self.conv4_3 = nn.Conv2d(512, 512, 3, padding=1)
        self.relu4_3 = nn.ReLU(inplace=True)
        self.pool4 = nn.MaxPool2d(2, stride=2, ceil_mode=True)

        # Block 5
        self.conv5_1 = nn.Conv2d(512, 512, 3, padding=1)
        self.relu5_1 = nn.ReLU(inplace=True)
        self.conv5_2 = nn.Conv2d(512, 512, 3, padding=1)
        self.relu5_2 = nn.ReLU(inplace=True)
        self.conv5_3 = nn.Conv2d(512, 512, 3, padding=1)
        self.relu5_3 = nn.ReLU(inplace=True)
        self.pool5 = nn.MaxPool2d(2, stride=2, ceil_mode=True)

        # FC6-7替换为卷积
        self.fc6 = nn.Conv2d(512, 4096, 7)
        self.relu6 = nn.ReLU(inplace=True)
        self.drop6 = nn.Dropout2d()

        self.fc7 = nn.Conv2d(4096, 4096, 1)
        self.relu7 = nn.ReLU(inplace=True)
        self.drop7 = nn.Dropout2d()

        # 分类层
        self.score_fr = nn.Conv2d(4096, num_classes, 1)
        self.score_pool3 = nn.Conv2d(256, num_classes, 1)
        self.score_pool4 = nn.Conv2d(512, num_classes, 1)

        # 上采样层
        self.upscore2 = nn.ConvTranspose2d(
            num_classes, num_classes, 4, stride=2, bias=False)
        self.upscore8 = nn.ConvTranspose2d(
            num_classes, num_classes, 16, stride=8, bias=False)
        self.upscore_pool4 = nn.ConvTranspose2d(
            num_classes, num_classes, 4, stride=2, bias=False)

    def forward(self, x):
        h = x

        # VGG卷积
        h = self.relu1_1(self.conv1_1(h))
        h = self.relu1_2(self.conv1_2(h))
        h = self.pool1(h)

        h = self.relu2_1(self.conv2_1(h))
        h = self.relu2_2(self.conv2_2(h))
        h = self.pool2(h)

        h = self.relu3_1(self.conv3_1(h))
        h = self.relu3_2(self.conv3_2(h))
        h = self.relu3_3(self.conv3_3(h))
        h = self.pool3(h)
        pool3 = h  # 保存pool3用于跳跃连接

        h = self.relu4_1(self.conv4_1(h))
        h = self.relu4_2(self.conv4_2(h))
        h = self.relu4_3(self.conv4_3(h))
        h = self.pool4(h)
        pool4 = h  # 保存pool4

        h = self.relu5_1(self.conv5_1(h))
        h = self.relu5_2(self.conv5_2(h))
        h = self.relu5_3(self.conv5_3(h))
        h = self.pool5(h)

        # FC层
        h = self.relu6(self.fc6(h))
        h = self.drop6(h)

        h = self.relu7(self.fc7(h))
        h = self.drop7(h)

        h = self.score_fr(h)
        h = self.upscore2(h)
        upscore2 = h  # 2倍上采样

        # 融合pool4
        h = self.score_pool4(pool4)
        h = h[:, :, 5:5+upscore2.size()[2], 5:5+upscore2.size()[3]]
        score_pool4c = h

        h = upscore2 + score_pool4c
        h = self.upscore_pool4(h)
        upscore_pool4 = h

        # 融合pool3
        h = self.score_pool3(pool3)
        h = h[:, :, 9:9+upscore_pool4.size()[2], 9:9+upscore_pool4.size()[3]]
        score_pool3c = h

        h = upscore_pool4 + score_pool3c
        h = self.upscore8(h)
        h = h[:, :, 31:31+x.size()[2], 31:31+x.size()[3]].contiguous()

        return h

11.1.3 训练技巧#

1. 损失函数#

像素级交叉熵损失:

criterion = nn.CrossEntropyLoss(ignore_index=255)
loss = criterion(output, target)

类别加权(处理类别不平衡):

# 计算类别权重
class_weights = compute_class_weight('balanced', classes, labels)
criterion = nn.CrossEntropyLoss(weight=torch.FloatTensor(class_weights))

2. 评估指标#

IoU(Intersection over Union)

def compute_iou(pred, target, num_classes):
    ious = []
    pred = pred.view(-1)
    target = target.view(-1)

    for cls in range(num_classes):
        pred_cls = (pred == cls)
        target_cls = (target == cls)

        intersection = (pred_cls & target_cls).sum().float()
        union = (pred_cls | target_cls).sum().float()

        if union == 0:
            ious.append(float('nan'))
        else:
            ious.append((intersection / union).item())

    return ious

# mIoU (mean IoU)
mean_iou = np.nanmean(ious)

11.1.4 FCN的局限#

  1. 感受野受限:由backbone决定
  2. 边界粗糙:8倍下采样仍有细节丢失
  3. 小物体效果差:多次下采样导致信息损失
  4. 训练慢:需要大量像素级标注

11.2 U-Net:编码器-解码器架构#

论文:U-Net: Convolutional Networks for Biomedical Image Segmentation (MICCAI 2015) 作者:Olaf Ronneberger, Philipp Fischer, Thomas Brox

11.2.1 为什么U-Net如此成功?#

U-Net最初为医学图像分割设计,但其架构设计如此优雅,成为了语义分割的经典范式。

核心优势

  1. 对称的编码器-解码器结构
  2. 密集的跳跃连接
  3. 数据增强策略
  4. 少量数据也能训练

11.2.2 架构设计#

U型结构#

输入图像 (572×572×1)
【编码器 Encoder】下采样路径
    conv 3×3, ReLU
    conv 3×3, ReLU
    max pool 2×2  → (280×280×64)  ─────┐
    ↓                                   │
    conv 3×3, ReLU                      │
    conv 3×3, ReLU                      │
    max pool 2×2  → (136×136×128) ─────┼───┐
    ↓                                   │   │
    conv 3×3, ReLU                      │   │
    conv 3×3, ReLU                      │   │
    max pool 2×2  → (64×64×256)   ─────┼───┼───┐
    ↓                                   │   │   │
    conv 3×3, ReLU                      │   │   │
    conv 3×3, ReLU                      │   │   │
    max pool 2×2  → (28×28×512)   ─────┼───┼───┼───┐
    ↓                                   │   │   │   │
【Bottleneck】                          │   │   │   │
    conv 3×3, ReLU                      │   │   │   │
    conv 3×3, ReLU  → (24×24×1024)      │   │   │   │
    ↓                                   │   │   │   │
【解码器 Decoder】上采样路径             │   │   │   │
    up-conv 2×2 → (48×48×512)           │   │   │   │
    concat ←────────────────────────────┘   │   │   │
    conv 3×3, ReLU                          │   │   │
    conv 3×3, ReLU                          │   │   │
    ↓                                       │   │   │
    up-conv 2×2 → (96×96×256)               │   │   │
    concat ←────────────────────────────────┘   │   │
    conv 3×3, ReLU                              │   │
    conv 3×3, ReLU                              │   │
    ↓                                           │   │
    up-conv 2×2 → (192×192×128)                 │   │
    concat ←────────────────────────────────────┘   │
    conv 3×3, ReLU                                  │
    conv 3×3, ReLU                                  │
    ↓                                               │
    up-conv 2×2 → (384×384×64)                      │
    concat ←────────────────────────────────────────┘
    conv 3×3, ReLU
    conv 3×3, ReLU
    conv 1×1 → (388×388×2)  输出

关键组件#

1. 下采样路径(Encoder)

def down_block(in_channels, out_channels):
    return nn.Sequential(
        nn.Conv2d(in_channels, out_channels, 3, padding=1),
        nn.ReLU(inplace=True),
        nn.Conv2d(out_channels, out_channels, 3, padding=1),
        nn.ReLU(inplace=True)
    )

2. 上采样路径(Decoder)

def up_block(in_channels, out_channels):
    return nn.Sequential(
        nn.ConvTranspose2d(in_channels, out_channels, 2, stride=2),
        nn.Conv2d(out_channels, out_channels, 3, padding=1),
        nn.ReLU(inplace=True),
        nn.Conv2d(out_channels, out_channels, 3, padding=1),
        nn.ReLU(inplace=True)
    )

3. 跳跃连接(Skip Connections)

U-Net的跳跃连接通过**通道拼接(concatenation)**实现:

# 编码器特征
encoder_feature = encoder(x)  # (B, C, H, W)

# 解码器上采样
decoder_upsampled = upsample(decoder_input)  # (B, C, H, W)

# 拼接
combined = torch.cat([encoder_feature, decoder_upsampled], dim=1)  # (B, 2C, H, W)

# 继续卷积
output = conv(combined)

为什么用concat而不是add?

  • concat保留更多信息
  • 让网络学习如何融合特征
  • 提供更丰富的梯度

11.2.3 完整实现#

见代码文件:unet_segmentation.py

11.2.4 训练策略#

1. 数据增强#

医学图像数据少,需要强力增强:

import albumentations as A

train_transform = A.Compose([
    A.HorizontalFlip(p=0.5),
    A.VerticalFlip(p=0.5),
    A.RandomRotate90(p=0.5),
    A.ShiftScaleRotate(shift_limit=0.0625, scale_limit=0.1, rotate_limit=45, p=0.5),
    A.ElasticTransform(alpha=1, sigma=50, p=0.5),  # 弹性变形
    A.GridDistortion(p=0.5),  # 网格扭曲
    A.RandomBrightnessContrast(p=0.3),
    A.Normalize(mean=(0.485,), std=(0.229,))
])

2. 加权损失#

处理边界和小目标:

class WeightedBCELoss(nn.Module):
    def __init__(self, weight_map):
        super().__init__()
        self.weight_map = weight_map

    def forward(self, pred, target):
        bce = F.binary_cross_entropy_with_logits(pred, target, reduction='none')
        weighted_bce = bce * self.weight_map
        return weighted_bce.mean()

# 计算权重图
def compute_weight_map(mask):
    # 边界权重更高
    from scipy.ndimage import distance_transform_edt

    distances = distance_transform_edt(mask == 0)
    weights = np.exp(-distances ** 2 / (2 * sigma ** 2))
    return weights

3. Dice Loss#

医学图像分割常用Dice Loss:

class DiceLoss(nn.Module):
    def __init__(self, smooth=1.0):
        super().__init__()
        self.smooth = smooth

    def forward(self, pred, target):
        pred = torch.sigmoid(pred)

        intersection = (pred * target).sum(dim=(2, 3))
        union = pred.sum(dim=(2, 3)) + target.sum(dim=(2, 3))

        dice = (2. * intersection + self.smooth) / (union + self.smooth)
        return 1 - dice.mean()

# 组合损失
class CombinedLoss(nn.Module):
    def __init__(self, alpha=0.5):
        super().__init__()
        self.alpha = alpha
        self.bce = nn.BCEWithLogitsLoss()
        self.dice = DiceLoss()

    def forward(self, pred, target):
        return self.alpha * self.bce(pred, target) + (1 - self.alpha) * self.dice(pred, target)

11.2.5 U-Net变体#

1. Attention U-Net#

添加注意力机制:

class AttentionBlock(nn.Module):
    def __init__(self, F_g, F_l, F_int):
        super().__init__()
        self.W_g = nn.Conv2d(F_g, F_int, 1, stride=1, padding=0)
        self.W_x = nn.Conv2d(F_l, F_int, 1, stride=1, padding=0)
        self.psi = nn.Conv2d(F_int, 1, 1, stride=1, padding=0)
        self.relu = nn.ReLU(inplace=True)
        self.sigmoid = nn.Sigmoid()

    def forward(self, g, x):
        g1 = self.W_g(g)
        x1 = self.W_x(x)
        psi = self.relu(g1 + x1)
        psi = self.sigmoid(self.psi(psi))
        return x * psi  # 加权特征

2. Res-UNet#

加入残差连接:

class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1)
        self.bn2 = nn.BatchNorm2d(out_channels)

        self.shortcut = nn.Sequential()
        if in_channels != out_channels:
            self.shortcut = nn.Conv2d(in_channels, out_channels, 1)

    def forward(self, x):
        residual = self.shortcut(x)

        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += residual
        out = F.relu(out)

        return out

3. U-Net++#

多尺度嵌套U-Net:

X^0,0 → X^0,1 → X^0,2 → X^0,3 → X^0,4
  ↓ ↘     ↓ ↘     ↓ ↘     ↓ ↘
X^1,0 → X^1,1 → X^1,2 → X^1,3
  ↓ ↘     ↓ ↘     ↓ ↘
X^2,0 → X^2,1 → X^2,2
  ↓ ↘     ↓ ↘
X^3,0 → X^3,1
  ↓ ↘
X^4,0

11.3 DeepLab系列:空洞卷积#

论文系列

  • DeepLab v1 (ICLR 2015)
  • DeepLab v2 (TPAMI 2017)
  • DeepLab v3 (arXiv 2017)
  • DeepLab v3+ (ECCV 2018)

11.3.1 核心创新:空洞卷积#

标准卷积的局限

为了扩大感受野,需要:

  1. 增加卷积层深度
  2. 使用更大的卷积核
  3. 池化操作

但这些都会导致:

  • 分辨率下降
  • 参数量增加
  • 计算成本上升

空洞卷积(Atrous Convolution / Dilated Convolution)

在不增加参数和计算量的情况下,扩大感受野。

原理

标准3×3卷积(rate=1):

× × ×
× × ×
× × ×

空洞卷积(rate=2):

×  ·  ×  ·  ×
·  ·  ·  ·  ·
×  ·  ×  ·  ×
·  ·  ·  ·  ·
×  ·  ×  ·  ×

空洞卷积(rate=4):

×  ·  ·  ·  ×  ·  ·  ·  ×
·  ·  ·  ·  ·  ·  ·  ·  ·
·  ·  ·  ·  ·  ·  ·  ·  ·
·  ·  ·  ·  ·  ·  ·  ·  ·
×  ·  ·  ·  ×  ·  ·  ·  ×

PyTorch实现

# 标准卷积:感受野3×3
conv = nn.Conv2d(64, 64, kernel_size=3, padding=1, dilation=1)

# 空洞卷积:感受野5×5,但参数量相同
atrous_conv = nn.Conv2d(64, 64, kernel_size=3, padding=2, dilation=2)

# 空洞卷积:感受野9×9
atrous_conv2 = nn.Conv2d(64, 64, kernel_size=3, padding=4, dilation=4)

感受野计算

receptive_field = (kernel_size - 1) * dilation + 1

kernel=3, dilation=1 → RF = 3
kernel=3, dilation=2 → RF = 5
kernel=3, dilation=4 → RF = 9
kernel=3, dilation=8 → RF = 17

11.3.2 ASPP(Atrous Spatial Pyramid Pooling)#

灵感来源:SPP(Spatial Pyramid Pooling)

核心思想:并行使用多个不同膨胀率的空洞卷积,捕获多尺度信息。

DeepLab v2的ASPP

输入特征图
    ├── rate=6  空洞卷积  ──┐
    ├── rate=12 空洞卷积  ──┤
    ├── rate=18 空洞卷积  ──┤ Concatenate
    └── rate=24 空洞卷积  ──┘
       1×1卷积融合
        输出特征

DeepLab v3的ASPP

改进版本,添加全局池化:

class ASPP(nn.Module):
    def __init__(self, in_channels, out_channels=256):
        super().__init__()

        # 1×1卷积
        self.conv1 = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, 1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )

        # 3×3空洞卷积,rate=6
        self.conv2 = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, 3, padding=6, dilation=6, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )

        # 3×3空洞卷积,rate=12
        self.conv3 = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, 3, padding=12, dilation=12, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )

        # 3×3空洞卷积,rate=18
        self.conv4 = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, 3, padding=18, dilation=18, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )

        # 全局平均池化
        self.global_avg_pool = nn.Sequential(
            nn.AdaptiveAvgPool2d((1, 1)),
            nn.Conv2d(in_channels, out_channels, 1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )

        # 融合
        self.project = nn.Sequential(
            nn.Conv2d(out_channels * 5, out_channels, 1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5)
        )

    def forward(self, x):
        size = x.shape[-2:]

        # 并行分支
        feat1 = self.conv1(x)
        feat2 = self.conv2(x)
        feat3 = self.conv3(x)
        feat4 = self.conv4(x)
        feat5 = F.interpolate(self.global_avg_pool(x), size=size, mode='bilinear', align_corners=True)

        # 拼接
        x = torch.cat([feat1, feat2, feat3, feat4, feat5], dim=1)

        # 融合
        x = self.project(x)
        return x

11.3.3 DeepLab v3+架构#

DeepLab v3+结合了:

  • Encoder-Decoder结构(借鉴U-Net)
  • ASPP模块(多尺度特征)
  • Xception Backbone(深度可分离卷积)

整体架构

输入图像
【Encoder】
Xception Backbone
    ├── 低层特征 (1/4) ──────────────┐
    ↓                              │
ASPP模块                           │
    ↓                              │
1×1卷积 (降维到256)                 │
    ↓                              │
上采样4倍                           │
    ↓                              │
【Decoder】                         │
    ├─ 拼接 ←──────────────────────┘
3×3卷积
3×3卷积
上采样4倍
输出 (原始分辨率)

使用torchvision实现

import torchvision.models.segmentation as segmentation

# 加载预训练DeepLab v3
model = segmentation.deeplabv3_resnet50(pretrained=True, num_classes=21)

# 或使用MobileNetV3作为backbone(更轻量)
model = segmentation.deeplabv3_mobilenet_v3_large(pretrained=True, num_classes=21)

# 推理
model.eval()
with torch.no_grad():
    output = model(image)['out']  # 注意返回字典
    prediction = torch.argmax(output, dim=1)

11.3.4 性能对比#

PASCAL VOC 2012 test set

模型BackbonemIoU (%)
FCN-8sVGG1662.2
DeepLab v2ResNet-10179.7
DeepLab v3ResNet-10185.7
DeepLab v3+Xception-6587.8
DeepLab v3+Xception-7189.0

优势

  • 空洞卷积:保持分辨率
  • ASPP:多尺度感受野
  • Decoder:恢复边界细节

11.4 实战:医学图像分割#

11.4.1 项目目标#

使用U-Net实现肺部CT图像的分割。

数据集:Medical Segmentation Decathlon - Lung Task

  • 训练集:64个CT扫描
  • 每个扫描包含多个切片
  • 标注:肺部区域mask

11.4.2 数据准备#

import nibabel as nib  # 读取医学图像格式
import numpy as np
from torch.utils.data import Dataset

class LungCTDataset(Dataset):
    def __init__(self, image_paths, mask_paths, transform=None):
        self.image_paths = image_paths
        self.mask_paths = mask_paths
        self.transform = transform

    def __len__(self):
        return len(self.image_paths)

    def __getitem__(self, idx):
        # 读取NIfTI格式
        image = nib.load(self.image_paths[idx]).get_fdata()
        mask = nib.load(self.mask_paths[idx]).get_fdata()

        # 归一化
        image = (image - image.min()) / (image.max() - image.min())

        # 转换为tensor
        image = torch.FloatTensor(image).unsqueeze(0)  # (1, H, W)
        mask = torch.LongTensor(mask)

        if self.transform:
            # 应用数据增强
            augmented = self.transform(image=image.numpy(), mask=mask.numpy())
            image = torch.FloatTensor(augmented['image'])
            mask = torch.LongTensor(augmented['mask'])

        return image, mask

11.4.3 训练流程#

完整代码见:code/chapter11_semantic_seg/unet_segmentation.py

11.4.4 评估指标#

def compute_dice_score(pred, target):
    """Dice系数"""
    smooth = 1.0
    pred_flat = pred.view(-1)
    target_flat = target.view(-1)

    intersection = (pred_flat * target_flat).sum()
    dice = (2. * intersection + smooth) / (pred_flat.sum() + target_flat.sum() + smooth)

    return dice.item()

def compute_hausdorff_distance(pred, target):
    """Hausdorff距离(边界精度)"""
    from scipy.spatial.distance import directed_hausdorff

    pred_points = np.argwhere(pred)
    target_points = np.argwhere(target)

    forward = directed_hausdorff(pred_points, target_points)[0]
    backward = directed_hausdorff(target_points, pred_points)[0]

    return max(forward, backward)

本章小结#

核心知识点#

  1. FCN(2015)

    • 全卷积网络,开创像素级预测
    • 转置卷积上采样
    • 跳跃连接融合多尺度特征
  2. U-Net(2015)

    • 对称的编码器-解码器结构
    • 密集的跳跃连接(concatenation)
    • 医学图像分割的金标准
    • 少量数据也能训练
  3. DeepLab系列(2015-2018)

    • 空洞卷积:扩大感受野,保持分辨率
    • ASPP:多尺度特征提取
    • Encoder-Decoder:恢复边界细节

技术对比#

模型核心技术优势局限
FCN全卷积+跳跃连接开创性,简单边界粗糙
U-Net对称结构+密集连接少量数据,边界精确内存占用大
DeepLab空洞卷积+ASPP多尺度,高分辨率计算量大

实践建议#

  1. 医学图像分割:首选U-Net
  2. 自然图像分割:DeepLab v3+
  3. 实时应用:轻量化backbone(MobileNet)
  4. 小数据集:强数据增强 + U-Net
  5. 类别不平衡:Dice Loss + Focal Loss

下一步#

第12章,我们将学习实例分割,了解如何区分同一类别的不同个体,掌握Mask R-CNN和YOLOv8-Seg的实现。


参考资源


第12章:实例分割#

从Mask R-CNN到YOLOv8-Seg,掌握实例级分割技术

本章概览#

实例分割(Instance Segmentation)是目标检测和语义分割的结合:不仅要识别每个像素的类别,还要区分同一类别的不同个体。

核心内容

  • Mask R-CNN如何扩展Faster R-CNN
  • YOLACT如何实现实时实例分割
  • YOLOv8-Seg的统一框架设计
  • COCO实例分割实战项目

实例分割 vs 语义分割#

任务对比#

场景:街道上有3辆汽车

【语义分割】输出:
  像素分类图:
  - 像素(100,200) → 类别: 汽车
  - 像素(300,150) → 类别: 汽车
  - 像素(500,400) → 类别: 汽车
  所有汽车像素标记为同一类别

【实例分割】输出:
  实例掩码:
  - 汽车1的mask
  - 汽车2的mask
  - 汽车3的mask
  每辆汽车有独立的mask

应用场景#

任务类型应用场景示例
语义分割场景理解道路分割、天空分割
实例分割个体计数车辆统计、人群分析
实例分割机器人抓取识别每个物体的位置
实例分割医学分析细胞计数、器官分割

12.1 Mask R-CNN原理#

论文:Mask R-CNN (ICCV 2017) 作者:Kaiming He, Georgia Gkioxari, Piotr Dollár, Ross Girshick (Facebook AI Research)

12.1.1 核心思想#

Mask R-CNN = Faster R-CNN + Mask分支

Faster R-CNN回顾

输入图像
Backbone (ResNet/FPN)
RPN (Region Proposal Network)
RoI Pooling
分类 + 边界框回归

Mask R-CNN扩展

输入图像
Backbone + FPN
RPN
RoI Align (替代RoI Pooling)
    ├── 分类分支 → 类别
    ├── 边界框分支 → 坐标
    └── Mask分支 → 像素级掩码

12.1.2 关键创新#

1. RoI Align#

RoI Pooling的问题

RoI Pooling涉及两次量化(quantization):

  1. 将RoI边界量化到特征图网格
  2. 将RoI划分为bins时的量化

示例

原始RoI: [12.4, 8.7, 45.2, 31.9]
量化后:   [12,   8,   45,   31  ]  ← 丢失小数部分

这种量化会导致像素级对齐误差,对分割任务影响很大。

RoI Align的解决方案

使用双线性插值避免量化:

# RoI Align伪代码
def roi_align(feature_map, roi, output_size):
    """
    feature_map: 特征图
    roi: [x1, y1, x2, y2](保留小数)
    output_size: 输出尺寸 (7, 7)
    """
    # 计算每个bin的精确位置(不量化)
    bin_height = (roi[3] - roi[1]) / output_size[0]
    bin_width = (roi[2] - roi[0]) / output_size[1]

    output = []
    for i in range(output_size[0]):
        for j in range(output_size[1]):
            # 计算bin的精确边界
            y_start = roi[1] + i * bin_height
            x_start = roi[0] + j * bin_width

            # 在bin内采样4个点(或更多)
            sampling_points = [
                (y_start + 0.25 * bin_height, x_start + 0.25 * bin_width),
                (y_start + 0.25 * bin_height, x_start + 0.75 * bin_width),
                (y_start + 0.75 * bin_height, x_start + 0.25 * bin_width),
                (y_start + 0.75 * bin_height, x_start + 0.75 * bin_width),
            ]

            # 对每个采样点使用双线性插值
            values = [bilinear_interpolate(feature_map, y, x) for y, x in sampling_points]

            # 聚合(max pooling或average)
            output.append(max(values))

    return output

效果对比

  • RoI Pooling:AP50 = 55.3%
  • RoI Align:AP50 = 58.7% (+3.4%)

2. Mask分支#

架构设计

class MaskHead(nn.Module):
    def __init__(self, in_channels=256, num_classes=80):
        super().__init__()

        # 4个3×3卷积(保持分辨率)
        self.conv1 = nn.Conv2d(in_channels, 256, 3, padding=1)
        self.conv2 = nn.Conv2d(256, 256, 3, padding=1)
        self.conv3 = nn.Conv2d(256, 256, 3, padding=1)
        self.conv4 = nn.Conv2d(256, 256, 3, padding=1)

        # 转置卷积(上采样2倍)
        self.deconv = nn.ConvTranspose2d(256, 256, 2, stride=2)

        # 每个类别一个mask
        self.predictor = nn.Conv2d(256, num_classes, 1)

    def forward(self, x):
        # x: RoI Align输出 (N, 256, 14, 14)

        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        x = F.relu(self.conv3(x))
        x = F.relu(self.conv4(x))

        x = F.relu(self.deconv(x))  # (N, 256, 28, 28)

        mask = self.predictor(x)  # (N, num_classes, 28, 28)

        return mask

特点

  • 输入:14×14的RoI特征
  • 输出:28×28的mask预测(每个类别一个)
  • 使用FCN风格的全卷积架构

3. 损失函数#

Mask R-CNN的总损失:

L = L_cls + L_box + L_mask

其中:
  L_cls: 分类损失(交叉熵)
  L_box: 边界框回归损失(Smooth L1)
  L_mask: Mask损失(二元交叉熵)

关键设计:Mask损失是per-class

def mask_loss(mask_pred, mask_target, labels):
    """
    mask_pred: (N, num_classes, H, W)
    mask_target: (N, H, W)
    labels: (N,) - 每个RoI的真实类别
    """
    N = mask_pred.size(0)
    loss = 0

    for i in range(N):
        # 只计算真实类别的mask损失
        cls = labels[i]
        pred = mask_pred[i, cls]  # (H, W)
        target = mask_target[i]   # (H, W)

        # 二元交叉熵
        loss += F.binary_cross_entropy_with_logits(pred, target)

    return loss / N

这样设计的好处:

  • 分类和分割解耦
  • 避免类间竞争
  • 每个类别独立优化

12.1.3 使用Detectron2#

Facebook开源的Detectron2提供了完整的Mask R-CNN实现:

from detectron2.engine import DefaultPredictor
from detectron2.config import get_cfg
from detectron2 import model_zoo
import cv2

# 配置
cfg = get_cfg()
cfg.merge_from_file(model_zoo.get_config_file("COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml"))
cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url("COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml")
cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.5  # 置信度阈值
cfg.MODEL.DEVICE = "cuda"

# 创建预测器
predictor = DefaultPredictor(cfg)

# 推理
image = cv2.imread("test.jpg")
outputs = predictor(image)

# 提取结果
instances = outputs["instances"].to("cpu")
boxes = instances.pred_boxes.tensor.numpy()  # 边界框
masks = instances.pred_masks.numpy()  # 实例掩码 (N, H, W)
classes = instances.pred_classes.numpy()  # 类别
scores = instances.scores.numpy()  # 置信度

# 可视化
from detectron2.utils.visualizer import Visualizer
from detectron2.data import MetadataCatalog

v = Visualizer(image[:, :, ::-1], MetadataCatalog.get(cfg.DATASETS.TRAIN[0]), scale=1.2)
out = v.draw_instance_predictions(instances)
cv2.imwrite("result.jpg", out.get_image()[:, :, ::-1])

12.1.4 Mask R-CNN变体#

1. Cascade Mask R-CNN#

多阶段精化:

输入图像
RPN
Stage 1: IoU threshold=0.5
Stage 2: IoU threshold=0.6
Stage 3: IoU threshold=0.7
最终预测

2. PointRend#

高分辨率边界精化:

粗糙mask (28×28)
上采样到原始分辨率
选择不确定的点(边界附近)
对这些点进行高分辨率推理
精细mask

12.2 YOLACT:实时实例分割#

论文:YOLACT: Real-time Instance Segmentation (ICCV 2019) 作者:Daniel Bolya et al. (UC Davis)

12.2.1 核心思想#

Mask R-CNN的问题

  • 串行处理:先检测再分割
  • 速度慢:~5 FPS

YOLACT的方案:并行生成原型mask和系数

输入图像
    ├── Protonet分支 → 原型masks (k个基础mask)
    └── 检测分支 → 边界框 + 类别 + mask系数
            线性组合原型masks
            实例masks

数学表达

M = σ(P × C^T)

其中:
  M: 最终mask (H×W)
  P: 原型masks (H×W×k)
  C: mask系数 (k×1)
  σ: sigmoid激活

12.2.2 架构设计#

1. Protonet#

生成k个原型masks:

class Protonet(nn.Module):
    def __init__(self, in_channels=256, num_protos=32):
        super().__init__()

        self.conv1 = nn.Conv2d(in_channels, 256, 3, padding=1)
        self.conv2 = nn.Conv2d(256, 256, 3, padding=1)
        self.conv3 = nn.Conv2d(256, 256, 3, padding=1)

        # 上采样
        self.upsample = nn.Upsample(scale_factor=2, mode='bilinear')

        # 生成原型
        self.final_conv = nn.Conv2d(256, num_protos, 1)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        x = F.relu(self.conv3(x))

        x = self.upsample(x)

        protos = self.final_conv(x)  # (B, k, H, W)
        return protos

2. Prediction Head#

在检测头添加mask系数预测:

class PredictionHead(nn.Module):
    def __init__(self, in_channels=256, num_classes=80, num_protos=32):
        super().__init__()

        # 类别预测
        self.class_conv = nn.Conv2d(in_channels, num_classes, 3, padding=1)

        # 边界框预测
        self.box_conv = nn.Conv2d(in_channels, 4, 3, padding=1)

        # Mask系数预测
        self.coef_conv = nn.Conv2d(in_channels, num_protos, 3, padding=1)

    def forward(self, x):
        classes = self.class_conv(x)
        boxes = self.box_conv(x)
        coefs = self.coef_conv(x)  # (B, k, H, W)

        return classes, boxes, coefs

3. Mask Assembly#

组装最终mask:

def assemble_masks(protos, coefs, boxes):
    """
    protos: (H, W, k) - 原型masks
    coefs: (n, k) - n个实例的系数
    boxes: (n, 4) - 边界框
    """
    # 线性组合
    masks = torch.matmul(protos, coefs.t())  # (H, W, n)
    masks = torch.sigmoid(masks)

    # 裁剪到边界框内
    masks = crop_masks(masks, boxes)

    return masks

12.2.3 性能特点#

COCO test-dev

模型BackbonemAPFPS (Titan Xp)
Mask R-CNNResNet-101-FPN37.15
YOLACT-550ResNet-101-FPN29.833
YOLACT-700ResNet-101-FPN31.223

优势

  • 速度快:30+ FPS
  • 端到端训练
  • 架构简单

劣势

  • 精度略低于Mask R-CNN
  • 原型数量k需要调优

12.3 YOLOv8-Seg:YOLO的分割版本#

发布时间:2023年1月 开发者:Ultralytics

12.3.1 核心特性#

YOLOv8-Seg = YOLOv8检测 + 分割头

架构

输入图像
【Backbone】CSPDarknet + C2f
【Neck】PAN-FPN
    ├── 检测分支
    │   ├── 类别预测
    │   └── 边界框预测
    └── 分割分支
        ├── Prototype生成(类似YOLACT)
        └── Mask系数预测

12.3.2 模型变体#

YOLOv8-Seg提供5个尺寸:

模型mAP^boxmAP^mask速度 (A100)参数量
YOLOv8n-seg36.730.596 FPS3.4M
YOLOv8s-seg44.636.877 FPS11.8M
YOLOv8m-seg49.940.853 FPS27.3M
YOLOv8l-seg52.342.639 FPS46.0M
YOLOv8x-seg53.443.428 FPS71.8M

YOLO11-Seg更新(2024年9月):

模型mAP^boxmAP^mask参数量改进
YOLO11n-seg38.932.02.9M更轻量
YOLO11s-seg46.637.810.1M+2.0 mAP
YOLO11m-seg51.541.522.4M+1.6 mAP
YOLO11l-seg53.442.927.6M+1.1 mAP
YOLO11x-seg54.743.862.1M+1.3 mAP

12.3.3 使用示例#

见代码文件:yolov8_instance_seg.py

12.3.4 训练自定义数据#

1. 数据格式#

YOLO分割格式(每行一个实例):

class_id x1 y1 x2 y2 x3 y3 ... xn yn

坐标归一化到[0, 1]。

示例

# person.txt
0 0.1 0.2 0.3 0.2 0.3 0.8 0.1 0.8  # 人的轮廓多边形

2. 数据集配置#

# custom_seg.yaml
path: /path/to/dataset
train: images/train
val: images/val

nc: 2  # 类别数
names: ['person', 'car']

3. 训练代码#

from ultralytics import YOLO

# 加载预训练模型
model = YOLO('yolo11n-seg.pt')

# 训练
results = model.train(
    data='custom_seg.yaml',
    epochs=100,
    imgsz=640,
    batch=16,
    device=0,
    workers=8,

    # 分割特定参数
    overlap_mask=True,  # mask重叠
    mask_ratio=4,  # mask下采样比例

    # 数据增强
    degrees=10.0,
    translate=0.1,
    scale=0.5,
    mosaic=1.0,
    mixup=0.1,
)

12.4 实战:实例分割项目#

12.4.1 项目目标#

使用YOLO11-Seg在COCO数据集上进行实例分割。

任务

  1. 加载预训练模型
  2. 在自定义图像上推理
  3. 可视化分割结果
  4. 导出ONNX模型

12.4.2 完整代码#

见代码文件:yolov8_instance_seg.py

12.4.3 评估指标#

1. Mask mAP#

与目标检测mAP类似,但用mask IoU代替box IoU:

def compute_mask_iou(mask1, mask2):
    """计算两个mask的IoU"""
    intersection = np.logical_and(mask1, mask2).sum()
    union = np.logical_or(mask1, mask2).sum()

    if union == 0:
        return 0.0
    return intersection / union

# COCO评估
from pycocotools.cocoeval import COCOeval

coco_eval = COCOeval(coco_gt, coco_dt, 'segm')
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()

2. 边界精度#

def boundary_iou(pred_mask, gt_mask, dilation_ratio=0.02):
    """计算边界IoU"""
    from scipy.ndimage import binary_dilation

    # 提取边界
    pred_boundary = binary_dilation(pred_mask) ^ pred_mask
    gt_boundary = binary_dilation(gt_mask) ^ gt_mask

    # 计算IoU
    intersection = (pred_boundary & gt_boundary).sum()
    union = (pred_boundary | gt_boundary).sum()

    return intersection / union if union > 0 else 0

12.4.4 后处理技巧#

1. Mask平滑#

from scipy.ndimage import gaussian_filter

def smooth_mask(mask, sigma=1.0):
    """高斯平滑mask边界"""
    smoothed = gaussian_filter(mask.astype(float), sigma=sigma)
    return (smoothed > 0.5).astype(np.uint8)

2. 小区域过滤#

def remove_small_regions(mask, min_area=100):
    """移除小区域"""
    from skimage.measure import label, regionprops

    labeled = label(mask)
    for region in regionprops(labeled):
        if region.area < min_area:
            mask[labeled == region.label] = 0

    return mask

3. Mask融合#

处理重叠mask:

def merge_overlapping_masks(masks, scores, iou_threshold=0.5):
    """合并重叠的mask"""
    keep = []

    # 按置信度排序
    order = np.argsort(scores)[::-1]

    while order.size > 0:
        i = order[0]
        keep.append(i)

        # 计算与其他mask的IoU
        ious = [compute_mask_iou(masks[i], masks[j]) for j in order[1:]]

        # 保留IoU低的mask
        inds = np.where(np.array(ious) <= iou_threshold)[0]
        order = order[inds + 1]

    return keep

本章小结#

核心知识点#

  1. Mask R-CNN(2017)

    • RoI Align:精确的特征对齐
    • 独立的Mask分支
    • Per-class mask预测
    • 两阶段架构
  2. YOLACT(2019)

    • 原型masks + 线性组合
    • 实时性能(30+ FPS)
    • 并行处理
  3. YOLOv8-Seg / YOLO11-Seg(2023-2024)

    • 统一框架,多任务支持
    • Anchor-free设计
    • 实时性能 + 高精度
    • 易于训练和部署

模型对比#

模型架构速度精度特点
Mask R-CNN两阶段~5 FPS精度高,速度慢
YOLACT单阶段30+ FPS实时,精度中等
YOLOv8-Seg单阶段50+ FPS中高平衡性能
YOLO11-Seg单阶段50+ FPS当前最优

选择建议#

需求推荐模型理由
最高精度Mask R-CNN学术研究
实时应用YOLO11-Seg工业部署
边缘设备YOLO11n-seg轻量级
视频分析YOLOv8-Seg速度快

实践建议#

  1. 数据准备

    • 使用COCO格式或YOLO格式
    • 确保mask质量高
    • 充分的数据增强
  2. 训练技巧

    • 先用检测模型预训练
    • 冻结backbone加速训练
    • 使用组合损失(box + mask)
  3. 后处理

    • Mask平滑
    • 小区域过滤
    • 重叠处理

下一步#

第13章,我们将学习Segment Anything (SAM),探索零样本分割的革命性技术,了解如何通过Prompt实现"万物分割"。


参考资源


第13章:Segment Anything (SAM)#

Meta的"万物分割"模型,零样本分割的革命性突破

本章概览#

Segment Anything Model (SAM) 是Meta AI在2023年4月发布的foundation model,开启了"万物分割"的新时代。SAM具有强大的零样本分割能力,可以在没有训练的情况下分割任意物体。

核心内容

  • SAM的模型架构和训练策略
  • Prompt Engineering:点、框、mask提示
  • SAM 2:扩展到视频分割
  • 实战:零样本分割应用

为什么SAM是革命性的?#

1. 零样本分割能力#

传统分割模型

# 只能分割训练时见过的类别
model_coco = SegmentationModel(classes=['person', 'car', 'dog'])
mask = model_coco(image)  # 只能分割这3类

SAM

# 可以分割任意物体,无需训练
model_sam = SAM()
mask = model_sam(image, point=[100, 200])  # 分割点击的任何物体

2. Promptable分割#

SAM支持多种提示方式:

提示类型描述应用场景
点击(Point)点击物体内部或外部交互式分割
边界框(Box)框选感兴趣区域快速标注
Mask提供粗糙mask精细化分割
文本(间接)通过Grounding DINO转换语言驱动分割

3. 强大的泛化能力#

  • 在SA-1B数据集上训练(11亿mask)
  • 覆盖广泛的场景和物体
  • 对新领域有良好的适应性

13.1 SAM模型架构#

13.1.1 整体架构#

SAM采用三阶段架构:

输入图像 + Prompt
【Image Encoder】
Vision Transformer (ViT)
Image Embeddings (一次性计算)
【Prompt Encoder】
    ├── 点 → 位置编码
    ├── 框 → 位置编码
    └── Mask → 卷积嵌入
Prompt Embeddings
【Mask Decoder】
Transformer Decoder
输出Masks (多个候选)

13.1.2 Image Encoder#

架构:Vision Transformer (ViT)

SAM使用预训练的ViT作为图像编码器:

class ImageEncoderViT(nn.Module):
    """
    SAM的图像编码器:ViT-H/16

    配置:
    - 模型:ViT-Huge
    - Patch size:16×16
    - 输入分辨率:1024×1024
    - 嵌入维度:1280
    - 层数:32
    - 注意力头:16
    """
    def __init__(self):
        super().__init__()

        # Patch Embedding
        self.patch_embed = nn.Conv2d(3, 1280, kernel_size=16, stride=16)

        # Position Embedding
        self.pos_embed = nn.Parameter(torch.zeros(1, 64*64, 1280))

        # Transformer Blocks
        self.blocks = nn.ModuleList([
            TransformerBlock(dim=1280, num_heads=16)
            for _ in range(32)
        ])

        # Neck(降维)
        self.neck = nn.Sequential(
            nn.Conv2d(1280, 256, 1),
            LayerNorm2d(256),
            nn.Conv2d(256, 256, 3, padding=1),
            LayerNorm2d(256),
        )

    def forward(self, x):
        # x: (B, 3, 1024, 1024)

        # Patch Embedding
        x = self.patch_embed(x)  # (B, 1280, 64, 64)
        x = x.flatten(2).transpose(1, 2)  # (B, 4096, 1280)

        # Add Position Embedding
        x = x + self.pos_embed

        # Transformer Blocks
        for block in self.blocks:
            x = block(x)

        # Reshape
        x = x.transpose(1, 2).reshape(B, 1280, 64, 64)

        # Neck
        x = self.neck(x)  # (B, 256, 64, 64)

        return x

特点

  • 输入固定为1024×1024
  • 只需计算一次(可缓存)
  • 使用MAE预训练权重

13.1.3 Prompt Encoder#

点和框的编码

class PromptEncoder(nn.Module):
    def __init__(self, embed_dim=256, image_size=1024):
        super().__init__()

        # 位置编码
        self.pe_layer = PositionalEncoding(embed_dim // 2)

        # 点类型编码(前景/背景)
        self.point_embeddings = nn.Embedding(2, embed_dim)

        # 不提供prompt时的嵌入
        self.not_a_point_embed = nn.Embedding(1, embed_dim)

    def _embed_points(self, points, labels):
        """
        points: (B, N, 2) - 归一化坐标 [0, 1]
        labels: (B, N) - 1=前景, 0=背景, -1=ignore
        """
        # 位置编码
        point_embedding = self.pe_layer(points)  # (B, N, 256)

        # 添加类型编码
        point_embedding += self.point_embeddings(labels)

        return point_embedding

    def _embed_boxes(self, boxes):
        """
        boxes: (B, 4) - [x1, y1, x2, y2] 归一化
        """
        # 框编码为2个点(左上 + 右下)
        top_left = boxes[:, :2]  # (B, 2)
        bottom_right = boxes[:, 2:]

        corner_embedding = torch.cat([
            self._embed_points(top_left, torch.ones(B, 1)),
            self._embed_points(bottom_right, torch.ones(B, 1))
        ], dim=1)

        return corner_embedding

    def _embed_masks(self, masks):
        """
        masks: (B, 1, H, W) - 提示mask
        """
        # 使用卷积编码
        mask_embedding = self.mask_downscaling(masks)  # (B, 256, 64, 64)
        return mask_embedding

Mask的编码

self.mask_downscaling = nn.Sequential(
    nn.Conv2d(1, 4, kernel_size=2, stride=2),
    LayerNorm2d(4),
    nn.GELU(),
    nn.Conv2d(4, 16, kernel_size=2, stride=2),
    LayerNorm2d(16),
    nn.GELU(),
    nn.Conv2d(16, 256, kernel_size=1),
)

13.1.4 Mask Decoder#

轻量级Transformer Decoder

class MaskDecoder(nn.Module):
    def __init__(
        self,
        transformer_dim=256,
        num_multimask_outputs=3,
    ):
        super().__init__()

        # Transformer解码器
        self.transformer = TwoWayTransformer(
            depth=2,
            embedding_dim=transformer_dim,
            num_heads=8,
            mlp_dim=2048,
        )

        # Mask tokens(可学习的query)
        self.iou_token = nn.Embedding(1, transformer_dim)
        self.mask_tokens = nn.Embedding(num_multimask_outputs + 1, transformer_dim)

        # 输出MLP
        self.iou_prediction_head = MLP(transformer_dim, transformer_dim, 1, 3)

        self.mask_prediction_heads = nn.ModuleList([
            MLP(transformer_dim, transformer_dim, 256, 3)
            for _ in range(num_multimask_outputs + 1)
        ])

    def forward(self, image_embeddings, prompt_embeddings):
        """
        image_embeddings: (B, 256, 64, 64)
        prompt_embeddings: (B, N, 256)
        """
        B = image_embeddings.shape[0]

        # 准备tokens
        tokens = torch.cat([
            self.iou_token.weight,  # (1, 256)
            self.mask_tokens.weight,  # (4, 256)
        ], dim=0).unsqueeze(0).expand(B, -1, -1)  # (B, 5, 256)

        # 组合prompt
        tokens = torch.cat([tokens, prompt_embeddings], dim=1)

        # Transformer解码
        src = image_embeddings.flatten(2).permute(0, 2, 1)  # (B, 4096, 256)
        hs, src = self.transformer(src, tokens)

        # 提取IoU token和mask tokens
        iou_token_out = hs[:, 0, :]  # (B, 256)
        mask_tokens_out = hs[:, 1:5, :]  # (B, 4, 256)

        # 上采样到原始分辨率
        src = src.transpose(1, 2).reshape(B, 256, 64, 64)

        # 预测masks
        masks = []
        for i, mlp in enumerate(self.mask_prediction_heads):
            mask_emb = mlp(mask_tokens_out[:, i, :])  # (B, 256)
            mask = (mask_emb.unsqueeze(-1).unsqueeze(-1) @ src.flatten(2)).reshape(B, 64, 64)
            masks.append(mask)

        masks = torch.stack(masks, dim=1)  # (B, 4, 64, 64)

        # 预测IoU
        iou_pred = self.iou_prediction_head(iou_token_out)

        return masks, iou_pred

输出

  • 3个候选mask + 1个低质量mask
  • 每个mask的IoU预测
  • 选择IoU最高的mask作为最终结果

13.2 Prompt Engineering for SAM#

13.2.1 点击式分割#

单点提示

from transformers import SamModel, SamProcessor
import torch

# 加载模型
model = SamModel.from_pretrained("facebook/sam-vit-huge")
processor = SamProcessor.from_pretrained("facebook/sam-vit-huge")

# 准备输入
image = Image.open("cat.jpg")
input_points = [[[500, 375]]]  # 点击猫的中心

inputs = processor(image, input_points=input_points, return_tensors="pt")

# 推理
with torch.no_grad():
    outputs = model(**inputs)

# 后处理
masks = processor.image_processor.post_process_masks(
    outputs.pred_masks.cpu(),
    inputs["original_sizes"].cpu(),
    inputs["reshaped_input_sizes"].cpu()
)

# 选择最佳mask
iou_scores = outputs.iou_scores
best_mask = masks[0][0, torch.argmax(iou_scores), :, :].numpy()

多点提示

# 前景点 + 背景点
input_points = [[[500, 375], [100, 100]]]  # 2个点
point_labels = [[1, 0]]  # 1=前景, 0=背景

inputs = processor(
    image,
    input_points=input_points,
    input_labels=point_labels,
    return_tensors="pt"
)

迭代优化

# 第1次:点击物体
points = [[500, 375]]
labels = [1]

# 获取初步mask
mask1 = get_mask(points, labels)

# 第2次:添加背景点(去除多余区域)
points.append([100, 100])
labels.append(0)

# 获取改进的mask
mask2 = get_mask(points, labels)

13.2.2 边界框提示#

框选物体

# 定义边界框 [x1, y1, x2, y2]
input_boxes = [[[100, 100, 500, 400]]]

inputs = processor(image, input_boxes=input_boxes, return_tensors="pt")

outputs = model(**inputs)
masks = processor.post_process_masks(outputs.pred_masks)

框 + 点组合

# 粗略框选 + 精确点击
input_boxes = [[[100, 100, 500, 400]]]
input_points = [[[300, 250]]]
point_labels = [[1]]

inputs = processor(
    image,
    input_boxes=input_boxes,
    input_points=input_points,
    input_labels=point_labels,
    return_tensors="pt"
)

13.2.3 Mask提示#

从粗糙mask到精细mask

# 提供一个粗糙的mask(例如从其他模型得到)
rough_mask = get_rough_mask(image)  # (H, W)

# 使用SAM精细化
input_masks = rough_mask.unsqueeze(0).unsqueeze(0)  # (1, 1, H, W)

inputs = processor(image, input_masks=input_masks, return_tensors="pt")
outputs = model(**inputs)

# 获取精细化的mask
refined_mask = processor.post_process_masks(outputs.pred_masks)[0]

13.2.4 自动分割(Everything Mode)#

生成所有可能的mask

from segment_anything import SamAutomaticMaskGenerator, sam_model_registry

# 加载模型
sam = sam_model_registry["vit_h"](checkpoint="sam_vit_h.pth")
mask_generator = SamAutomaticMaskGenerator(
    model=sam,
    points_per_side=32,  # 网格点数
    pred_iou_thresh=0.88,
    stability_score_thresh=0.95,
    crop_n_layers=1,
    crop_n_points_downscale_factor=2,
    min_mask_region_area=100,  # 最小区域
)

# 生成所有masks
masks = mask_generator.generate(image_np)

# masks是一个列表,每个元素包含:
# - segmentation: (H, W) bool array
# - area: int
# - bbox: [x, y, w, h]
# - predicted_iou: float
# - stability_score: float

13.3 SAM 2:视频分割#

发布时间:2024年7月 论文:SAM 2: Segment Anything in Images and Videos

13.3.1 核心改进#

SAM 2扩展了SAM的能力:

  1. 统一的图像和视频架构
  2. 流式记忆(Streaming Memory)
  3. 实时视频对象跟踪

架构对比

【SAM】
图像 → Image Encoder → Prompt Encoder → Mask Decoder → Mask

【SAM 2】
视频帧序列 → Image Encoder → Memory Bank ← 历史帧
                        Prompt Encoder + Memory Attention
                        Mask Decoder → Masks

13.3.2 Memory Mechanism#

流式记忆

class MemoryBank:
    def __init__(self, max_size=7):
        self.memory = []
        self.max_size = max_size

    def update(self, frame_embedding, mask):
        """添加新帧的特征和mask"""
        self.memory.append({
            'embedding': frame_embedding,
            'mask': mask,
        })

        # 保持固定大小
        if len(self.memory) > self.max_size:
            self.memory.pop(0)

    def get_context(self):
        """获取历史上下文"""
        if not self.memory:
            return None

        embeddings = [m['embedding'] for m in self.memory]
        masks = [m['mask'] for m in self.memory]

        return torch.stack(embeddings), torch.stack(masks)

Memory Attention

class MemoryAttention(nn.Module):
    def forward(self, query, memory_bank):
        """
        query: 当前帧的特征
        memory_bank: 历史帧的特征和mask
        """
        if memory_bank is None:
            return query

        mem_embeddings, mem_masks = memory_bank.get_context()

        # Cross-attention
        attended = self.cross_attention(
            query=query,
            key=mem_embeddings,
            value=mem_embeddings
        )

        return attended

13.3.3 使用SAM 2#

安装

git clone https://github.com/facebookresearch/sam2.git
cd sam2
pip install -e .

视频对象分割

from sam2.build_sam import build_sam2_video_predictor

# 加载模型
predictor = build_sam2_video_predictor(
    "configs/sam2.1/sam2.1_hiera_large.yaml",
    "checkpoints/sam2.1_hiera_large.pt"
)

# 初始化视频
with torch.inference_mode(), torch.autocast("cuda", dtype=torch.bfloat16):
    state = predictor.init_state(video_path="video.mp4")

    # 在第一帧添加点击
    frame_idx, object_ids, masks = predictor.add_new_points_or_box(
        state,
        frame_idx=0,
        obj_id=0,
        points=np.array([[500, 375]], dtype=np.float32),
        labels=np.array([1], dtype=np.int32)
    )

    # 传播到所有帧
    for frame_idx, object_ids, masks in predictor.propagate_in_video(state):
        # masks: (num_objects, H, W)
        # 保存或显示masks
        save_masks(frame_idx, masks)

多对象跟踪

# 添加多个对象
predictor.add_new_points_or_box(state, frame_idx=0, obj_id=0, points=[[100, 200]], labels=[1])
predictor.add_new_points_or_box(state, frame_idx=0, obj_id=1, points=[[500, 300]], labels=[1])

# 传播
for frame_idx, object_ids, masks in predictor.propagate_in_video(state):
    # masks[0]: 对象0的mask
    # masks[1]: 对象1的mask
    pass

13.3.4 SAM 2性能#

SA-V (Segment Anything Video) 数据集

模型J&F (%)FPS参数量
SAM 2.1 Tiny76.591.238.9M
SAM 2.1 Small76.684.846M
SAM 2.1 Base Plus78.264.180.8M
SAM 2.1 Large79.539.5224.4M

改进

  • 支持视频(SAM仅支持图像)
  • 实时性能(91 FPS for Tiny)
  • 更好的时序一致性

13.4 实战:零样本分割应用#

13.4.1 项目:交互式图像编辑#

完整代码见:sam_zero_shot.py

13.4.2 应用场景#

1. 快速数据标注#

# 标注工作流
def annotation_workflow(images, output_dir):
    sam = load_sam_model()

    for img_path in images:
        image = load_image(img_path)

        # 用户点击
        points = get_user_clicks(image)

        # SAM自动分割
        mask = sam(image, points=points)

        # 保存标注
        save_annotation(mask, output_dir / img_path.name)

2. 移除背景#

def remove_background(image_path, point):
    """一键移除背景"""
    sam = load_sam_model()
    image = load_image(image_path)

    # 点击前景物体
    mask = sam(image, points=[point], labels=[1])

    # 提取前景
    foreground = image * mask[:, :, None]

    # 透明背景
    rgba = np.concatenate([foreground, mask[:, :, None] * 255], axis=2)

    return rgba

3. 对象替换#

def object_replacement(base_image, object_image, point):
    """替换物体"""
    # 分割出要替换的区域
    mask = sam(base_image, points=[point])

    # 将新物体放置到该区域
    result = base_image.copy()
    result[mask] = object_image[mask]

    return result

13.4.3 SAM的局限#

  1. 不支持类别识别

    • SAM只分割,不识别类别
    • 需要结合分类模型
  2. 小物体效果一般

    • 对小物体(<32×32)效果不佳
  3. 边界精度

    • 某些情况下边界不够精细
    • 可能需要后处理
  4. 计算成本

    • ViT-Huge模型较大
    • 需要高性能GPU

13.4.4 SAM + 其他模型#

1. Grounded-SAM#

结合Grounding DINO实现文本驱动分割:

from groundingdino.util.inference import load_model, predict
from sam import sam_model_registry, SamPredictor

# Grounding DINO检测
boxes, logits, phrases = predict(
    model=grounding_dino,
    image=image,
    caption="a cat",
    box_threshold=0.35
)

# SAM分割
sam = SamPredictor(sam_model_registry["vit_h"]())
sam.set_image(image)

masks, _, _ = sam.predict(
    boxes=boxes,  # 使用检测框作为提示
    multimask_output=False
)

2. MobileSAM#

轻量化版本:

# 原始SAM: 636M参数
# MobileSAM: 仅9.7M参数,速度快10倍

from mobile_sam import sam_model_registry, SamPredictor

mobile_sam = sam_model_registry["vit_t"](checkpoint="mobile_sam.pt")
predictor = SamPredictor(mobile_sam)

本章小结#

核心知识点#

  1. SAM架构(2023)

    • Image Encoder (ViT-H)
    • Prompt Encoder (点/框/mask)
    • Mask Decoder (轻量级Transformer)
    • 零样本分割能力
  2. Prompt Engineering

    • 单点/多点提示
    • 边界框提示
    • Mask提示
    • Everything模式
  3. SAM 2(2024)

    • 统一图像和视频
    • 流式记忆机制
    • 实时视频对象分割
    • 多对象跟踪

技术对比#

特性SAMSAM 2传统分割
零样本
视频支持部分
交互式
实时性
精度取决于训练

应用场景#

场景SAM优势推荐方案
数据标注快速、准确SAM + 标注工具
图像编辑交互式SAM
视频编辑时序一致SAM 2
目标提取无需训练SAM + Grounding DINO
医学分割泛化能力强SAM + 微调

实践建议#

  1. 选择合适的模型尺寸

    • ViT-Huge: 最高精度
    • ViT-Large: 平衡
    • ViT-Base: 速度快
    • MobileSAM: 边缘设备
  2. Prompt策略

    • 简单物体:单点
    • 复杂物体:多点 + 框
    • 精细边界:迭代优化
  3. 后处理

    • Mask平滑
    • 小区域过滤
    • 边界精化

未来展望#

  1. 更小的模型:MobileSAM、FastSAM
  2. 更强的能力:3D分割、语义理解
  3. 更多应用:AR/VR、机器人、医疗

第五篇总结#

完整技术栈#

【语义分割】
FCN (2015) → U-Net (2015) → DeepLab v3+ (2018)

【实例分割】
Mask R-CNN (2017) → YOLACT (2019) → YOLOv8-Seg (2023) → YOLO11-Seg (2024)

【零样本分割】
SAM (2023) → SAM 2 (2024)

学习成果#

学完本篇后,你已经掌握:

  1. 理论基础

    • 分割任务的分类和特点
    • 编码器-解码器架构
    • 评估指标(IoU、Dice)
  2. 经典模型

    • FCN: 全卷积网络
    • U-Net: 医学图像分割
    • DeepLab: 空洞卷积
    • Mask R-CNN: 实例分割
  3. 前沿技术

    • YOLOv8/YOLO11-Seg: 实时分割
    • SAM: 零样本分割
    • SAM 2: 视频分割
  4. 实战能力

    • 医学图像分割项目
    • COCO实例分割
    • 交互式分割应用

下一步#

恭喜完成第五篇!接下来可以:

  1. 深入研究:阅读相关论文
  2. 实战项目:在自己的数据上训练
  3. 探索应用:结合实际需求开发应用
  4. 跟进前沿:关注最新研究进展

参考资源


[统计组件仅在生产环境显示]