跳转至

第 8 章 图像分割

⚠️ 时效性说明:本章涉及的模型性能数据(如 mIoU 、推理速度等)可能随新版本发布而变化;请以论文原文和官方发布页为准。这里更关注结构设计、损失函数与训练思路,而不是某一时点的固定榜单结果。

📚 章节概述

本章介绍图像分割的核心算法,包括语义分割、实例分割等。图像分割是像素级别的分类任务,广泛应用于医疗影像、自动驾驶、工业检测等领域。

学习时间: 5-7 天 难度等级:⭐⭐⭐⭐⭐ 前置知识:第 5-7 章

🎯 学习目标

完成本章后,你将能够: - 理解图像分割的任务和挑战 - 掌握 FCN 、 U-Net 等分割网络 - 了解 DeepLab 、 PSPNet 等高级方法 - 能够实现图像分割应用 - 完成图像分割项目


8.1 图像分割概述

8.1.1 任务定义

语义分割:像素级别的分类 - 同一类别的不同实例用相同颜色

实例分割:区分同一类别的不同实例 - 每个实例独立标记

8.1.2 评估指标

IoU (Intersection over Union)

Python
import numpy as np

def calculate_iou(pred, target, num_classes):
    """计算 mIoU

    这里采用“仅统计并集非空类别”的常见做法。
    若你的评测协议需要把缺失类别计为 1 或忽略背景,请按对应数据集规则调整。
    """
    ious = []
    for cls in range(num_classes):
        pred_mask = pred == cls
        target_mask = target == cls

        intersection = (pred_mask & target_mask).sum()
        union = (pred_mask | target_mask).sum()

        if union > 0:
            ious.append(intersection / union)

    if not ious:
        raise ValueError("当前样本中没有可计算 IoU 的类别")
    return float(np.mean(ious))

8.2 FCN (Fully Convolutional Networks)

核心思想: - 用卷积层代替全连接层 - 输出像素级别的预测 - 上采样恢复空间分辨率

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

class ToyFCN(nn.Module):
    """可运行的 FCN 最小实现,用于理解全卷积 + 上采样流程。"""

    def __init__(self, num_classes=21):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(inplace=True),
            nn.MaxPool2d(2, 2),
            nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(inplace=True),
            nn.MaxPool2d(2, 2),
            nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(inplace=True),
        )
        self.classifier = nn.Conv2d(256, num_classes, kernel_size=1)

    def forward(self, x):
        input_size = x.shape[-2:]
        features = self.encoder(x)
        logits = self.classifier(features)
        return F.interpolate(logits, size=input_size, mode='bilinear', align_corners=False)

8.3 U-Net

架构特点: - 编码器-解码器结构 - 跳跃连接( Skip Connection ) - 适用于小数据集

Python
import torch
import torch.nn as nn

class UNet(nn.Module):
    def __init__(self, in_channels=3, out_channels=1):
        super(UNet, self).__init__()

        # 编码器
        self.enc1 = self._block(in_channels, 64)
        self.enc2 = self._block(64, 128)
        self.enc3 = self._block(128, 256)
        self.enc4 = self._block(256, 512)

        # 瓶颈层
        self.bottleneck = self._block(512, 1024)

        # 解码器
        self.dec4 = self._block(512 + 512, 512)
        self.dec3 = self._block(256 + 256, 256)
        self.dec2 = self._block(128 + 128, 128)
        self.dec1 = self._block(64 + 64, 64)

        # 最终卷积
        self.final = nn.Conv2d(64, out_channels, 1)

        # 池化
        self.pool = nn.MaxPool2d(2, 2)

        # 上采样(转置卷积,同时将通道数减半)
        self.up4 = nn.ConvTranspose2d(1024, 512, 2, 2)
        self.up3 = nn.ConvTranspose2d(512, 256, 2, 2)
        self.up2 = nn.ConvTranspose2d(256, 128, 2, 2)
        self.up1 = nn.ConvTranspose2d(128, 64, 2, 2)

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

    def forward(self, x):
        # 编码器
        enc1 = self.enc1(x)
        enc2 = self.enc2(self.pool(enc1))
        enc3 = self.enc3(self.pool(enc2))
        enc4 = self.enc4(self.pool(enc3))

        # 瓶颈层
        bottleneck = self.bottleneck(self.pool(enc4))

        # 解码器(转置卷积上采样 + 跳跃连接拼接)
        dec4 = self.dec4(torch.cat([self.up4(bottleneck), enc4], dim=1))  # torch.cat沿已有维度拼接张量
        dec3 = self.dec3(torch.cat([self.up3(dec4), enc3], dim=1))
        dec2 = self.dec2(torch.cat([self.up2(dec3), enc2], dim=1))
        dec1 = self.dec1(torch.cat([self.up1(dec2), enc1], dim=1))

        # 输出
        return self.final(dec1)

8.4 DeepLab

核心创新: - 空洞卷积( Atrous Convolution ) - ASPP ( Atrous Spatial Pyramid Pooling ) - 在部分版本或特定数据集上可选配 CRF 等边界细化后处理

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

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

        # 不同膨胀率的卷积
        self.conv1 = nn.Conv2d(in_channels, out_channels, 1, bias=False)
        self.conv2 = nn.Conv2d(in_channels, out_channels, 3, padding=6, dilation=6, bias=False)
        self.conv3 = nn.Conv2d(in_channels, out_channels, 3, padding=12, dilation=12, bias=False)
        self.conv4 = nn.Conv2d(in_channels, out_channels, 3, padding=18, dilation=18, bias=False)

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

        # 融合
        self.conv_out = nn.Conv2d(out_channels * 5, out_channels, 1, bias=False)

    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)  # F.xxx PyTorch函数式API

        out = torch.cat([feat1, feat2, feat3, feat4, feat5], dim=1)
        out = self.conv_out(out)

        return out

8.5 实例分割: Mask R-CNN

架构: - Faster R-CNN + Mask 分支

Python
from torchvision.models.detection import maskrcnn_resnet50_fpn_v2, MaskRCNN_ResNet50_FPN_V2_Weights
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor

# torchvision 中直接提供可运行的 Mask R-CNN 实现,教学时优先基于官方实现做 finetune
num_classes = 2  # 1 个前景类别 + background
model = maskrcnn_resnet50_fpn_v2(weights=MaskRCNN_ResNet50_FPN_V2_Weights.DEFAULT)

# 替换 box predictor
in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)

# 替换 mask predictor
in_features_mask = model.roi_heads.mask_predictor.conv5_mask.in_channels
hidden_layer = 256
model.roi_heads.mask_predictor = MaskRCNNPredictor(in_features_mask, hidden_layer, num_classes)

8.6 实战案例:医学图像分割

Python
import torch
import torch.nn as nn
from torch.utils.data import DataLoader

# 损失函数
class DiceLoss(nn.Module):
    def __init__(self):
        super(DiceLoss, self).__init__()

    def forward(self, pred, target):
        # 该实现默认用于二值分割;多类分割通常需要按类别逐通道计算 Dice
        smooth = 1.0
        pred = torch.sigmoid(pred)
        pred_flat = pred.reshape(-1)
        target_flat = target.float().reshape(-1)

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

# 训练
def train_segmentation(model, dataloader, criterion, optimizer, epochs=100):
    if len(dataloader) == 0:
        raise ValueError("dataloader 为空,无法开始分割训练")
    model.train()  # train()训练模式
    for epoch in range(epochs):
        total_loss = 0.0
        for images, masks in dataloader:
            # 前向传播
            outputs = model(images)
            loss = criterion(outputs, masks)

            # 反向传播
            optimizer.zero_grad()  # 清零梯度
            loss.backward()  # 反向传播计算梯度
            optimizer.step()  # 更新参数

            total_loss += loss.item()  # 将单元素张量转为Python数值

        print(f'Epoch {epoch+1}, Loss: {total_loss/len(dataloader):.4f}')

8.7 练习题

基础题

  1. 简答题
  2. 语义分割和实例分割有什么区别?

    语义分割对每个像素分配类别标签,但不区分同类的不同个体(如所有人标为"人");实例分割不仅分类每个像素,还区分同类的不同实例(如"人 1""人 2"),是语义分割+实例区分的结合。

  3. U-Net 的跳跃连接有什么作用?

    跳跃连接将编码器各层的特征图拼接到解码器对应层,保留了高分辨率的细节信息(如边缘、纹理),弥补了下采样过程中丢失的空间信息,使分割边界更精确。

进阶题

  1. 编程题
  2. 实现一个简单的分割网络。
  3. 计算分割的 mIoU 指标。

8.8 关键复盘

高频复盘题

Q1: 什么是空洞卷积?它的作用是什么?

参考答案: - 定义:在卷积核元素之间插入空洞 - 作用:扩大感受野而不增加参数 - 应用: DeepLab 、多尺度特征提取

Q2: U-Net 为什么适合医学图像分割?

参考答案: - 编码器-解码器结构 - 跳跃连接保留细节 - 适用于小数据集 - 端到端训练


8.9 本章小结

核心知识点

  1. 语义分割:像素级分类
  2. 实例分割:区分实例
  3. FCN:全卷积网络
  4. U-Net:跳跃连接
  5. DeepLab:空洞卷积、 ASPP

下一步

下一章09-视频分析与理解.md - 学习视频分析


恭喜完成第 8 章! 🎉


最后更新日期: 2026-04-03