第五篇:图像分割与实例分割#
从像素级理解到万物分割,掌握图像分割的完整技术栈
篇章概览#
图像分割是计算机视觉的核心任务之一,它不仅要识别"哪里有物体"(目标检测),还要精确描绘"物体的每一个像素"。本篇将系统学习:
- 语义分割:为每个像素分配类别标签
- 实例分割:区分同一类别的不同个体
- 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 扩展到视频分割学习路径建议#
入门阶段#
- 理解语义分割与实例分割的区别
- 掌握FCN和U-Net的核心思想
- 实践医学图像分割项目
进阶阶段#
- 学习Mask R-CNN的两阶段设计
- 掌握YOLOv8-Seg的实时分割
- 理解空洞卷积的作用
高级阶段#
- 深入SAM的架构和Prompt机制
- 探索SAM 2的视频分割能力
- 应用于实际项目
实战项目#
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的快速标注
学习目标#
学完本篇后,你将能够:
理解分割任务
- 区分语义分割、实例分割、全景分割
- 掌握评估指标的计算和意义
掌握核心模型
- 实现U-Net从零开始
- 使用YOLOv8-Seg进行实例分割
- 应用SAM进行零样本分割
完成实战项目
- 医学图像分割
- COCO实例分割
- 自定义分割应用
解决实际问题
- 小目标分割优化
- 类别不平衡处理
- 实时性能优化
下一步#
准备好了吗?让我们从第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只猫” |
应用场景#
医学影像分析
- 器官分割(肝脏、肺部)
- 病灶检测(肿瘤、病变)
- 细胞分割
自动驾驶
- 道路分割
- 车道线检测
- 障碍物识别
遥感图像分析
- 土地利用分类
- 建筑物提取
- 森林监测
图像编辑
- 人像抠图
- 背景替换
- 风格迁移
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)
↓
卷积层 (提取特征)
↓
全连接层 (分类)
↓
输出:单一类别标签全连接层有两个问题:
- 固定输入尺寸:必须是224×224
- 丢失空间信息:输出是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-32s | 32倍 | 无 | 粗糙 |
| FCN-16s | 16倍 | pool4 | 改善 |
| FCN-8s | 8倍 | 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 h11.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的局限#
- 感受野受限:由backbone决定
- 边界粗糙:8倍下采样仍有细节丢失
- 小物体效果差:多次下采样导致信息损失
- 训练慢:需要大量像素级标注
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最初为医学图像分割设计,但其架构设计如此优雅,成为了语义分割的经典范式。
核心优势:
- 对称的编码器-解码器结构
- 密集的跳跃连接
- 数据增强策略
- 少量数据也能训练
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 weights3. 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 out3. 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,011.3 DeepLab系列:空洞卷积#
论文系列:
- DeepLab v1 (ICLR 2015)
- DeepLab v2 (TPAMI 2017)
- DeepLab v3 (arXiv 2017)
- DeepLab v3+ (ECCV 2018)
11.3.1 核心创新:空洞卷积#
标准卷积的局限:
为了扩大感受野,需要:
- 增加卷积层深度
- 使用更大的卷积核
- 池化操作
但这些都会导致:
- 分辨率下降
- 参数量增加
- 计算成本上升
空洞卷积(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 = 1711.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 x11.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:
| 模型 | Backbone | mIoU (%) |
|---|---|---|
| FCN-8s | VGG16 | 62.2 |
| DeepLab v2 | ResNet-101 | 79.7 |
| DeepLab v3 | ResNet-101 | 85.7 |
| DeepLab v3+ | Xception-65 | 87.8 |
| DeepLab v3+ | Xception-71 | 89.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, mask11.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)本章小结#
核心知识点#
FCN(2015)
- 全卷积网络,开创像素级预测
- 转置卷积上采样
- 跳跃连接融合多尺度特征
U-Net(2015)
- 对称的编码器-解码器结构
- 密集的跳跃连接(concatenation)
- 医学图像分割的金标准
- 少量数据也能训练
DeepLab系列(2015-2018)
- 空洞卷积:扩大感受野,保持分辨率
- ASPP:多尺度特征提取
- Encoder-Decoder:恢复边界细节
技术对比#
| 模型 | 核心技术 | 优势 | 局限 |
|---|---|---|---|
| FCN | 全卷积+跳跃连接 | 开创性,简单 | 边界粗糙 |
| U-Net | 对称结构+密集连接 | 少量数据,边界精确 | 内存占用大 |
| DeepLab | 空洞卷积+ASPP | 多尺度,高分辨率 | 计算量大 |
实践建议#
- 医学图像分割:首选U-Net
- 自然图像分割:DeepLab v3+
- 实时应用:轻量化backbone(MobileNet)
- 小数据集:强数据增强 + U-Net
- 类别不平衡: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):
- 将RoI边界量化到特征图网格
- 将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)
↓
上采样到原始分辨率
↓
选择不确定的点(边界附近)
↓
对这些点进行高分辨率推理
↓
精细mask12.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 protos2. 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, coefs3. 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 masks12.2.3 性能特点#
COCO test-dev:
| 模型 | Backbone | mAP | FPS (Titan Xp) |
|---|---|---|---|
| Mask R-CNN | ResNet-101-FPN | 37.1 | 5 |
| YOLACT-550 | ResNet-101-FPN | 29.8 | 33 |
| YOLACT-700 | ResNet-101-FPN | 31.2 | 23 |
优势:
- 速度快: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^box | mAP^mask | 速度 (A100) | 参数量 |
|---|---|---|---|---|
| YOLOv8n-seg | 36.7 | 30.5 | 96 FPS | 3.4M |
| YOLOv8s-seg | 44.6 | 36.8 | 77 FPS | 11.8M |
| YOLOv8m-seg | 49.9 | 40.8 | 53 FPS | 27.3M |
| YOLOv8l-seg | 52.3 | 42.6 | 39 FPS | 46.0M |
| YOLOv8x-seg | 53.4 | 43.4 | 28 FPS | 71.8M |
YOLO11-Seg更新(2024年9月):
| 模型 | mAP^box | mAP^mask | 参数量 | 改进 |
|---|---|---|---|---|
| YOLO11n-seg | 38.9 | 32.0 | 2.9M | 更轻量 |
| YOLO11s-seg | 46.6 | 37.8 | 10.1M | +2.0 mAP |
| YOLO11m-seg | 51.5 | 41.5 | 22.4M | +1.6 mAP |
| YOLO11l-seg | 53.4 | 42.9 | 27.6M | +1.1 mAP |
| YOLO11x-seg | 54.7 | 43.8 | 62.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数据集上进行实例分割。
任务:
- 加载预训练模型
- 在自定义图像上推理
- 可视化分割结果
- 导出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 012.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 mask3. 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本章小结#
核心知识点#
Mask R-CNN(2017)
- RoI Align:精确的特征对齐
- 独立的Mask分支
- Per-class mask预测
- 两阶段架构
YOLACT(2019)
- 原型masks + 线性组合
- 实时性能(30+ FPS)
- 并行处理
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 | 速度快 |
实践建议#
数据准备:
- 使用COCO格式或YOLO格式
- 确保mask质量高
- 充分的数据增强
训练技巧:
- 先用检测模型预训练
- 冻结backbone加速训练
- 使用组合损失(box + mask)
后处理:
- 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_embeddingMask的编码:
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: float13.3 SAM 2:视频分割#
发布时间:2024年7月 论文:SAM 2: Segment Anything in Images and Videos
13.3.1 核心改进#
SAM 2扩展了SAM的能力:
- 统一的图像和视频架构
- 流式记忆(Streaming Memory)
- 实时视频对象跟踪
架构对比:
【SAM】
图像 → Image Encoder → Prompt Encoder → Mask Decoder → Mask
【SAM 2】
视频帧序列 → Image Encoder → Memory Bank ← 历史帧
↓
Prompt Encoder + Memory Attention
↓
Mask Decoder → Masks13.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 attended13.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
pass13.3.4 SAM 2性能#
SA-V (Segment Anything Video) 数据集:
| 模型 | J&F (%) | FPS | 参数量 |
|---|---|---|---|
| SAM 2.1 Tiny | 76.5 | 91.2 | 38.9M |
| SAM 2.1 Small | 76.6 | 84.8 | 46M |
| SAM 2.1 Base Plus | 78.2 | 64.1 | 80.8M |
| SAM 2.1 Large | 79.5 | 39.5 | 224.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 rgba3. 对象替换#
def object_replacement(base_image, object_image, point):
"""替换物体"""
# 分割出要替换的区域
mask = sam(base_image, points=[point])
# 将新物体放置到该区域
result = base_image.copy()
result[mask] = object_image[mask]
return result13.4.3 SAM的局限#
不支持类别识别
- SAM只分割,不识别类别
- 需要结合分类模型
小物体效果一般
- 对小物体(<32×32)效果不佳
边界精度
- 某些情况下边界不够精细
- 可能需要后处理
计算成本
- 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)本章小结#
核心知识点#
SAM架构(2023)
- Image Encoder (ViT-H)
- Prompt Encoder (点/框/mask)
- Mask Decoder (轻量级Transformer)
- 零样本分割能力
Prompt Engineering
- 单点/多点提示
- 边界框提示
- Mask提示
- Everything模式
SAM 2(2024)
- 统一图像和视频
- 流式记忆机制
- 实时视频对象分割
- 多对象跟踪
技术对比#
| 特性 | SAM | SAM 2 | 传统分割 |
|---|---|---|---|
| 零样本 | ✓ | ✓ | ✗ |
| 视频支持 | ✗ | ✓ | 部分 |
| 交互式 | ✓ | ✓ | ✗ |
| 实时性 | 中 | 高 | 高 |
| 精度 | 高 | 高 | 取决于训练 |
应用场景#
| 场景 | SAM优势 | 推荐方案 |
|---|---|---|
| 数据标注 | 快速、准确 | SAM + 标注工具 |
| 图像编辑 | 交互式 | SAM |
| 视频编辑 | 时序一致 | SAM 2 |
| 目标提取 | 无需训练 | SAM + Grounding DINO |
| 医学分割 | 泛化能力强 | SAM + 微调 |
实践建议#
选择合适的模型尺寸:
- ViT-Huge: 最高精度
- ViT-Large: 平衡
- ViT-Base: 速度快
- MobileSAM: 边缘设备
Prompt策略:
- 简单物体:单点
- 复杂物体:多点 + 框
- 精细边界:迭代优化
后处理:
- Mask平滑
- 小区域过滤
- 边界精化
未来展望#
- 更小的模型:MobileSAM、FastSAM
- 更强的能力:3D分割、语义理解
- 更多应用: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)学习成果#
学完本篇后,你已经掌握:
理论基础
- 分割任务的分类和特点
- 编码器-解码器架构
- 评估指标(IoU、Dice)
经典模型
- FCN: 全卷积网络
- U-Net: 医学图像分割
- DeepLab: 空洞卷积
- Mask R-CNN: 实例分割
前沿技术
- YOLOv8/YOLO11-Seg: 实时分割
- SAM: 零样本分割
- SAM 2: 视频分割
实战能力
- 医学图像分割项目
- COCO实例分割
- 交互式分割应用
下一步#
恭喜完成第五篇!接下来可以:
- 深入研究:阅读相关论文
- 实战项目:在自己的数据上训练
- 探索应用:结合实际需求开发应用
- 跟进前沿:关注最新研究进展
参考资源: