目标检测与图像分割实战

目标检测和图像分割是计算机视觉中两个核心任务。比图像分类更进一步,它们不仅要知道”图中有什么”,还要知道”在哪里”。目标检测输出包围盒(Bounding Box),图像分割输出像素级的掩码。这两个任务在自动驾驶、医疗影像、视频监控等领域有广泛应用。

这篇文章会带你从目标检测的发展史讲起,理解YOLO系列和两阶段检测器的核心原理,然后进入图像分割领域,深入U-Net和Mask R-CNN的细节。最后用代码实战演示如何训练这些模型。

目标检测发展史

目标检测经历了从传统方法到深度学习的重大变革。传统方法如滑动窗口+HOG/SIFT+ SVM,需要人工设计特征,在复杂场景下效果有限。2012年AlexNet出现后,深度学习横扫计算机视觉各领域,目标检测也随之进入深度学习时代。

两阶段检测器:从R-CNN到Faster R-CNN

两阶段检测器的思路是先生成候选区域,再对每个区域进行分类和回归。“两步走”保证了检测精度,但速度相对慢一些。

**R-CNN(2014)**是开山之作。它用Selective Search生成2000个候选框,把每个候选框crop后送入AlexNet提取特征,再用SVM分类。问题很明显:每张图2000个候选框,每个都要过一遍CNN,太慢了——一张图要几十秒。

**Fast R-CNN(2015)**解决了重复CNN forward的问题。它对整张图做一次CNN,然后在特征图上RoI Pooling提取每个候选框的特征。这比R-CNN快了20多倍。

**Faster R-CNN(2015)**更近一步,用RPN(Region Proposal Network)替代了Selective Search,让候选框生成也变成可学习的。整个网络端到端训练,检测速度和精度都大幅提升。

单阶段检测器:YOLO与SSD

两阶段检测器精度高但速度慢,单阶段检测器则是速度优先、精度稍逊但已经足够好。

**YOLO(2015)**的思路是把目标检测当成一个回归问题:把图像分成SxS个格子,每个格子预测B个包围盒和C个类别概率。它真正实现了端到端,一张图只过一次网络,速度极快。但初代YOLO对小目标和密集目标的检测效果不好。

**SSD(2016)**在多个特征图尺度上做检测,兼顾了大目标和小目标的检测。浅层特征图分辨率大,适合检测小目标;深层特征图语义丰富,适合检测大目标。

# 展示不同检测器的特点
detectors_comparison = {
    'R-CNN': {'stage': 'Two-stage', 'speed': 'Slow (1 FPS)', 'accuracy': 'Baseline'},
    'Fast R-CNN': {'stage': 'Two-stage', 'speed': 'Medium (10 FPS)', 'accuracy': 'Good'},
    'Faster R-CNN': {'stage': 'Two-stage', 'speed': 'Medium-Fast (15-20 FPS)', 'accuracy': 'High'},
    'YOLOv1': {'stage': 'One-stage', 'speed': 'Fast (45 FPS)', 'accuracy': 'Moderate'},
    'SSD': {'stage': 'One-stage', 'speed': 'Very Fast (45-75 FPS)', 'accuracy': 'Good'},
    'YOLOv3': {'stage': 'One-stage', 'speed': 'Fast (50+ FPS)', 'accuracy': 'High'},
    'YOLOv8': {'stage': 'One-stage', 'speed': 'Fast (100+ FPS)', 'accuracy': 'Very High'},
    'DETR': {'stage': 'Transformer', 'speed': 'Medium (25 FPS)', 'accuracy': 'High'}
}
 
import pandas as pd
df = pd.DataFrame(detectors_comparison).T
print("目标检测器发展对比:")
print(df.to_string())

锚框机制与Anchor-Free检测

锚框(Anchor Box)是目标检测中的核心概念。它是在图像上预设的一系列不同尺寸和长宽比的参考框,模型的任务是预测每个锚框是否包含物体以及如何调整它来精确匹配物体。

为什么需要锚框

传统的滑动窗口方法要在所有可能的位置和尺度上检测,这是天文数字般的计算量。锚框把这个问题离散化了——预定义一组锚框(通常几十到上百个),模型只需要预测相对这些锚框的调整量。

锚框的设置需要精心设计。通常会设置多个尺度(如32²、64²、128²)和多个长宽比(如1:1、1:2、2:1)。尺度和长宽比要覆盖数据集中目标的大小分布。

Anchor-Free的崛起

锚框需要大量人工调参,而且对不同任务需要重新设计锚框。Anchor-Free方法应运而生,典型代表是FCOS(Fully Convolutional One-Stage)和CenterNet。

FCOS的思路是:直接预测每个像素到包围盒四条边的距离,而不是预测相对于锚框的偏移。这让它不需要预设锚框,而且能检测任意尺度的目标。

CenterNet更进一步:检测目标的中心点,然后预测中心点的属性(宽高、类别等)。它把目标检测变成了一个关键点检测问题,非常简洁。

# 对比Anchor-based和Anchor-free的差异
print("=" * 60)
print("Anchor-based vs Anchor-free")
print("=" * 60)
 
anchor_based = """
Anchor-based (YOLO, Faster R-CNN):
- 需要预设锚框尺寸和比例
- 需要IoU匹配正负样本
- 参数量大,召回率高
- 超参数多,需要针对数据集调优
"""
 
anchor_free = """
Anchor-free (FCOS, CenterNet):
- 直接预测位置和尺寸
- 无需预设锚框
- 参数量小,对小目标友好
- 后处理更简单(CenterNet用heatmap)
"""
 
print(anchor_based)
print(anchor_free)

YOLO系列演进详解

YOLO是目标检测领域最受欢迎的系列。从2015年的初代到现在的YOLOv8,每一代都有重要的改进。

YOLOv1到YOLOv3

YOLOv1(2015) 把图像分成7×7网格,每个格子预测2个包围盒和20个类别。但它只检测每格一个类别,对小目标和重叠目标效果很差。

YOLOv2(2016) 引入了Batch Normalization、高分辨率分类器、锚框机制、PassThrough层(融合浅层特征)等改进。Anchor Box从预设变成用K-Means在数据集上聚类得到。精度大幅提升。

YOLOv3(2018) 使用了Darknet-53主干网络、多尺度预测(FPN)、Logistic分类器(多标签而非多类独占)。对小目标检测有显著改进。

YOLOv4到YOLOv8

YOLOv4(2020) 集大成者,引入了CSPDarknet53主干、Mish激活、PANet颈网络、CIoU Loss、CmBN等大量技巧。在COCO上达到43.5% AP,媲美EfficientDet。

YOLOv5(2020) 由Ultralytics公司开源,代码质量极高,部署方便。它引入了AutoAnchor、自适应图片缩放、Mosaic增强等。成为工业界最常用的检测器。

YOLOv7(2022) 提出E-ELAN(扩展高效层聚合网络)、模型缩放、辅助头训练等技术,进一步提升精度和速度。

YOLOv8(2023) 采用anchor-free设计,引入C2f模块、新的损失函数(DFL),提供从n到x的5个规模版本,全面超越前代。Ultralytics还提供了完整的训练、验证、部署工具链。

# YOLOv8核心改进
yolov8_improvements = {
    'Backbone': {
        'C2f模块': 'CSPDarknet的改进版,梯度流更顺畅',
        'SPPF': '快速空间金字塔池化,多尺度特征融合'
    },
    'Neck': {
        'PAFPN': '路径聚合特征金字塔,融合不同层级特征'
    },
    'Head': {
        'Anchor-free': '无需预设锚框,更简洁的检测头',
        '解耦头': '分类和回归分开,更精确的预测'
    },
    'Loss': {
        'Bbox Loss': 'DFL (Distribution Focal Loss) + CIoU',
        'Cls Loss': 'VFL (Variable Focal Loss)',
        'DFL': '将边界框表示为连续分布,更好的定位'
    },
    'Data Augmentation': {
        'Mosaic': '拼接4张图增强',
        'MixUp': '两张图混合增强',
        'Copy-Paste': '复制粘贴增强'
    }
}
 
for component, improvements in yolov8_improvements.items():
    print(f"\n{component}:")
    for key, value in improvements.items():
        print(f"  {key}: {value}")

NMS与mAP评估指标

NMS非极大值抑制

目标检测模型会输出很多重叠的包围盒(同一个目标可能被多个框检测到)。NMS就是用来去除冗余框的。

NMS的步骤:

  1. 按置信度排序所有检测框
  2. 选取置信度最高的框作为基准
  3. 计算其他所有框与它的IoU
  4. IoU大于阈值的框被抑制
  5. 重复直到所有框都被处理

IoU阈值通常设为0.5。太高会漏检,太低会保留过多冗余框。

import numpy as np
 
def nms(boxes, scores, iou_threshold=0.5):
    """
    非极大值抑制
    boxes: [N, 4] 格式为 [x1, y1, x2, y2]
    scores: [N]
    """
    x1 = boxes[:, 0]
    y1 = boxes[:, 1]
    x2 = boxes[:, 2]
    y2 = boxes[:, 3]
    
    areas = (x2 - x1) * (y2 - y1)
    order = scores.argsort()[::-1]
    
    keep = []
    
    while order.size > 0:
        i = order[0]
        keep.append(i)
        
        # 计算与其他框的IoU
        xx1 = np.maximum(x1[i], x1[order[1:]])
        yy1 = np.maximum(y1[i], y1[order[1:]])
        xx2 = np.minimum(x2[i], x2[order[1:]])
        yy2 = np.minimum(y2[i], y2[order[1:]])
        
        w = np.maximum(0, xx2 - xx1)
        h = np.maximum(0, yy2 - yy1)
        
        intersection = w * h
        union = areas[i] + areas[order[1:]] - intersection
        iou = intersection / (union + 1e-10)
        
        # 保留IoU小于阈值的框
        inds = np.where(iou <= iou_threshold)[0]
        order = order[inds + 1]
    
    return keep
 
# 可视化NMS效果
def visualize_nms():
    """可视化NMS如何去除冗余框"""
    fig, axes = plt.subplots(1, 3, figsize=(15, 5))
    
    # 创建测试图像
    img = np.ones((100, 100, 3)) * 0.9
    
    # 模拟检测结果:同一目标有多个重叠框
    boxes = np.array([
        [30, 30, 70, 70],   # 真实框
        [28, 32, 72, 68],   # 稍微偏移
        [32, 28, 68, 72],   # 另一个偏移
        [25, 25, 75, 75],   # 更大的框
        [10, 10, 40, 40],   # 另一个目标
    ])
    scores = np.array([0.95, 0.90, 0.88, 0.85, 0.80])
    
    # NMS之前
    ax = axes[0]
    ax.imshow(img)
    colors = ['green', 'red', 'red', 'red', 'blue']
    for box, score, color in zip(boxes, scores, colors):
        rect = plt.Rectangle((box[0], box[1]), box[2]-box[0], box[3]-box[1],
                             fill=False, edgecolor=color, linewidth=2)
        ax.add_patch(rect)
        ax.text(box[0], box[1]-5, f'{score:.2f}', color=color, fontsize=10)
    ax.set_title('Before NMS\n(Green: Best, Red: Duplicates, Blue: Other)')
    ax.axis('off')
    
    # 应用NMS
    keep_indices = nms(boxes, scores, iou_threshold=0.5)
    boxes_nms = boxes[keep_indices]
    scores_nms = scores[keep_indices]
    
    # NMS之后
    ax = axes[1]
    ax.imshow(img)
    for box, score in zip(boxes_nms, scores_nms):
        rect = plt.Rectangle((box[0], box[1]), box[2]-box[0], box[3]-box[1],
                             fill=False, edgecolor='green', linewidth=2)
        ax.add_patch(rect)
        ax.text(box[0], box[1]-5, f'{score:.2f}', color='green', fontsize=10)
    ax.set_title('After NMS\n(Duplicates removed)')
    ax.axis('off')
    
    # IoU计算示意图
    ax = axes[2]
    box1 = [30, 30, 70, 70]
    box2 = [28, 32, 72, 68]
    
    ax.imshow(img)
    
    # 画两个框
    rect1 = plt.Rectangle((box1[0], box1[1]), box1[2]-box1[0], box1[3]-box1[1],
                           fill=True, facecolor='blue', alpha=0.3, edgecolor='blue', linewidth=2)
    rect2 = plt.Rectangle((box2[0], box2[1]), box2[2]-box2[0], box2[3]-box2[1],
                           fill=True, facecolor='red', alpha=0.3, edgecolor='red', linewidth=2)
    ax.add_patch(rect1)
    ax.add_patch(rect2)
    
    # 交集区域
    inter_x1, inter_y1 = max(box1[0], box2[0]), max(box1[1], box2[1])
    inter_x2, inter_y2 = min(box1[2], box2[2]), min(box1[3], box2[3])
    if inter_x2 > inter_x1 and inter_y2 > inter_y1:
        inter_rect = plt.Rectangle((inter_x1, inter_y1), inter_x2-inter_x1, inter_y2-inter_y1,
                                    fill=True, facecolor='yellow', alpha=0.5)
        ax.add_patch(inter_rect)
    
    ax.set_title(f'IoU = Intersection / Union\n{38*36/(40*40+42*36-38*36):.2f}')
    ax.axis('off')
    
    plt.tight_layout()
    plt.savefig('nms_visualization.png', dpi=150)
 
visualize_nms()

mAP评估指标

mAP(mean Average Precision)是目标检测最核心的评估指标。它是一系列AP的平均值,每个类别有一个AP。

IoU(Intersection over Union) 是预测框和真实框的重叠程度:交集面积 / 并集面积。IoU > 阈值(如0.5)才算检测正确。

Precision和Recall:和分类任务类似,但要对每个IoU阈值计算。

AP(Average Precision):precision-recall曲线下的面积。曲线越靠近右上角越好。

mAP@0.5:所有类别AP@0.5的平均值,IoU阈值固定为0.5。

mAP@[0.5:0.95]:IoU阈值从0.5到0.95,每隔0.05取一次,计算平均。更严格,衡量定位精度。

from sklearn.metrics import precision_recall_curve, average_precision_score
import matplotlib.pyplot as plt
 
def calculate_map(y_true_boxes, y_pred_boxes, num_classes, iou_threshold=0.5):
    """
    计算mAP
    y_true_boxes: list of dicts with 'bbox' and 'class'
    y_pred_boxes: list of dicts with 'bbox', 'class', and 'score'
    """
    aps = []
    
    for cls in range(num_classes):
        # 获取该类别的预测和真实框
        preds = [p for p in y_pred_boxes if p['class'] == cls]
        trues = [t for t in y_true_boxes if t['class'] == cls]
        
        if len(trues) == 0:
            continue
        
        # 按置信度排序
        preds.sort(key=lambda x: x['score'], reverse=True)
        
        # 累计计算precision和recall
        tp = np.zeros(len(preds))
        fp = np.zeros(len(preds))
        
        for i, pred in enumerate(preds):
            max_iou = 0
            max_idx = -1
            
            for j, true in enumerate(trues):
                if true.get('matched', False):
                    continue
                
                iou = calculate_iou(pred['bbox'], true['bbox'])
                if iou > max_iou:
                    max_iou = iou
                    max_idx = j
            
            if max_iou >= iou_threshold:
                tp[i] = 1
                trues[max_idx]['matched'] = True
            else:
                fp[i] = 1
        
        # 计算累计TP和FP
        tp_cumsum = np.cumsum(tp)
        fp_cumsum = np.cumsum(fp)
        
        recalls = tp_cumsum / len(trues)
        precisions = tp_cumsum / (tp_cumsum + fp_cumsum)
        
        # AP = precision-recall曲线下面积
        ap = np.trapz(precisions, recalls)
        aps.append(ap)
    
    return np.mean(aps)
 
def calculate_iou(box1, box2):
    """计算两个框的IoU"""
    x1 = max(box1[0], box2[0])
    y1 = max(box1[1], box2[1])
    x2 = min(box1[2], box2[2])
    y2 = min(box1[3], box2[3])
    
    inter = max(0, x2 - x1) * max(0, y2 - y1)
    
    area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
    area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
    
    union = area1 + area2 - inter
    
    return inter / (union + 1e-10)
 
# mAP可视化
print("=" * 60)
print("mAP计算说明")
print("=" * 60)
print("""
mAP@[0.5:0.95] 解读:
- 计算IoU阈值0.5, 0.55, 0.6, ..., 0.95的AP
- 对每个阈值分别计算precision-recall曲线下面积
- 最后取平均
 
mAP@0.5 解读:
- 只用IoU=0.5阈值
- 对定位精度要求较低
- 适合一般目标检测
 
mAP@0.75 解读:
- IoU=0.75,更严格
- 对定位精度要求高
- 适合需要精确包围盒的场景
""")

图像分割:像素级理解

图像分割比目标检测更进一步——不是输出包围盒,而是输出像素级的掩码,精确到每个像素属于哪个物体或类别。

三种分割任务

语义分割(Semantic Segmentation):对每个像素分类,但同类物体不区分。比如图像中有3个人,语义分割会把这3个人的像素都标为”人”。

实例分割(Instance Segmentation):不仅分类,还要区分同类物体的不同个体。同样3个人,实例分割会标出”人1”、“人2”、“人3”。

全景分割(Panoptic Segmentation):语义分割+实例分割的结合。背景类(草地、天空)用语义分割,前景物体用实例分割。全景分割是CVPR 2019提出的统一框架。

# 三种分割任务对比
segmentation_tasks = """
语义分割 (Semantic Segmentation):
- 输入: 图像
- 输出: 每个像素的类别标签
- 示例: FCN, DeepLab, U-Net
- 应用: 自动驾驶道路分割、医学影像
 
实例分割 (Instance Segmentation):  
- 输入: 图像
- 输出: 每个实例的掩码 + 类别
- 示例: Mask R-CNN, YOLACT
- 应用: 细胞计数、物体计数
 
全景分割 (Panoptic Segmentation):
- 输入: 图像
- 输出: 每个像素的语义标签 + 实例ID
- 示例: Panoptic FPN, UPSNet
- 应用: 完整场景理解
"""
print(segmentation_tasks)

U-Net:医学图像分割利器

U-Net是2015年提出的网络结构,最初用于医学图像的细胞分割。它采用编码器-解码器架构,编码器负责提取特征(类似VGG),解码器负责上采样恢复空间分辨率。关键创新是跳跃连接(Skip Connection),把编码器的特征图直接传到解码器对应层,弥补了下采样造成的位置信息丢失。

U-Net的结构对称,形似字母U,因此得名。它只需要很少的标注数据就能训练出很好的效果,非常适合医学图像这种标注成本高的领域。

U-Net核心组件

编码器(下采样路径):重复的3×3卷积 + ReLU + 2×2 Max Pooling。每次下采样特征图尺寸减半,通道数翻倍。

解码器(上采样路径):2×2反卷积(转置卷积)+ 跳跃连接拼接 + 3×3卷积 + ReLU。上采样恢复分辨率,同时通过跳跃连接获得细粒度信息。

瓶颈层:编码器和解码器之间的过渡,使用最深的特征表示。

import torch
import torch.nn as nn
import torch.nn.functional as F
 
class DoubleConv(nn.Module):
    """两个连续的卷积块"""
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.double_conv = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )
    
    def forward(self, x):
        return self.double_conv(x)
 
class UNet(nn.Module):
    def __init__(self, in_channels=3, out_channels=1, features=[64, 128, 256, 512]):
        super().__init__()
        self.downs = nn.ModuleList()
        self.ups = nn.ModuleList()
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
        
        # 编码器(下采样路径)
        for feature in features:
            self.downs.append(DoubleConv(in_channels, feature))
            in_channels = feature
        
        # 瓶颈层
        self.bottleneck = DoubleConv(features[-1], features[-1] * 2)
        
        # 解码器(上采样路径)
        for feature in reversed(features):
            # 上采样(转置卷积)
            self.ups.append(
                nn.ConvTranspose2d(feature * 2, feature, kernel_size=2, stride=2)
            )
            self.ups.append(DoubleConv(feature * 2, feature))
        
        # 输出层
        self.final_conv = nn.Conv2d(features[0], out_channels, kernel_size=1)
    
    def forward(self, x):
        skip_connections = []
        
        # 下采样
        for down in self.downs:
            x = down(x)
            skip_connections.append(x)
            x = self.pool(x)
        
        x = self.bottleneck(x)
        skip_connections = skip_connections[::-1]  # 反转用于对应跳跃连接
        
        # 上采样
        for idx in range(0, len(self.ups), 2):
            x = self.ups[idx](x)  # 上采样
            skip_connection = skip_connections[idx // 2]
            
            # 处理尺寸不匹配
            if x.shape != skip_connection.shape:
                x = F.interpolate(x, size=skip_connection.shape[2:])
            
            x = torch.cat([skip_connection, x], dim=1)  # 跳跃连接
            x = self.ups[idx + 1](x)  # DoubleConv
        
        return self.final_conv(x)
 
# 创建一个UNet实例并打印结构
unet = UNet(in_channels=3, out_channels=1)
print(f"U-Net 参数量: {sum(p.numel() for p in unet.parameters()):,}")
 
# 测试输入输出
test_input = torch.randn(1, 3, 572, 572)
test_output = unet(test_input)
print(f"输入尺寸: {test_input.shape}")
print(f"输出尺寸: {test_output.shape}")

Mask R-CNN:实例分割的标杆

Mask R-CNN是Faster R-CNN的扩展,在原有分类和回归头的基础上增加了一个分割头(Fcn),输出每个候选区域的掩码。

Mask R-CNN的关键改进是RoIAlign——替代Faster R-CNN中的RoI Pooling。RoI Pooling有两次量化误差:候选框坐标到特征图的映射、特征图到固定尺寸的池化。RoIAlign使用双线性插值避免了量化,精确对齐了特征图和RoI。

class MaskRCNN(nn.Module):
    """
    Mask R-CNN 简化实现
    实际使用请参考 torchvision.models.detection.maskrcnn_resnet50_fpn
    """
    def __init__(self, num_classes=91):  # COCO有91个类别
        super().__init__()
        # 骨干网络(FPN)
        self.backbone = BackboneWithFPN()
        
        # RPN (Region Proposal Network)
        self.rpn = RPN()
        
        # RoI Head
        self.roi_heads = RoIHeads(
            box_head=TwoMLPHead(),
            box_predictor=FastRCNNPredictor(),
            mask_head=MaskRCNNPredictor()
        )
    
    def forward(self, images, targets=None):
        """
        images: list of Tensor [C, H, W]
        targets: list of dict (训练时使用)
        """
        if self.training and targets is not None:
            # 训练模式
            proposals, loss_dict = self._forward_train(images, targets)
            return loss_dict
        else:
            # 推理模式
            predictions = self._forward_inference(images)
            return predictions
 
# 使用预训练的Mask R-CNN
print("=" * 60)
print("使用预训练Mask R-CNN进行实例分割")
print("=" * 60)
 
# 注意:这只是说明,实际运行需要安装torchvision
print("""
# torchvision有预训练的Mask R-CNN
from torchvision.models.detection import maskrcnn_resnet50_fpn
 
model = maskrcnn_resnet50_fpn(pretrained=True)
model.eval()
 
# 推理
with torch.no_grad():
    predictions = model(images)
    # predictions包含:
    # - boxes: 包围盒
    # - labels: 类别
    # - scores: 置信度
    # - masks: 实例掩码 (推理时才有)
""")

DeepLab系列:空洞卷积与ASPP

DeepLab是谷歌提出的语义分割系列,最新的DeepLabv3+结合了空洞卷积和编解码结构。

空洞卷积(Dilated/Atrous Convolution):在卷积核中插入空洞,在不增加参数量的情况下增大感受野。标准的3×3卷积加2个空洞,等效于7×7的感受野。

ASPP(Atrous Spatial Pyramid Pooling):在多个膨胀率的空洞卷积上并行提取特征,捕捉多尺度上下文信息。

class ASPP(nn.Module):
    """Atrous Spatial Pyramid Pooling"""
    def __init__(self, in_channels, out_channels, atrous_rates=[6, 12, 18]):
        super().__init__()
        
        modules = []
        # 1x1卷积
        modules.append(nn.Sequential(
            nn.Conv2d(in_channels, out_channels, 1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        ))
        
        # 多尺度空洞卷积
        for rate in atrous_rates:
            modules.append(nn.Sequential(
                nn.Conv2d(in_channels, out_channels, 3, padding=rate, 
                         dilation=rate, bias=False),
                nn.BatchNorm2d(out_channels),
                nn.ReLU(inplace=True)
            ))
        
        # 全局池化分支
        modules.append(nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Conv2d(in_channels, out_channels, 1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        ))
        
        self.convs = nn.ModuleList(modules)
        self.project = nn.Sequential(
            nn.Conv2d(len(modules) * out_channels, out_channels, 1, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )
    
    def forward(self, x):
        res = []
        for conv in self.convs:
            res.append(conv(x))
        
        # 全局池化需要上采样
        res[-1] = F.interpolate(res[-1], size=x.shape[2:], mode='bilinear', align_corners=False)
        
        res = torch.cat(res, dim=1)
        return self.project(res)
 
print("ASPP模块创建完成,用于多尺度特征提取")

代码实战:YOLOv8训练自定义数据集

现在来实战:用ultralytics的YOLOv8训练一个自定义目标检测数据集。

# YOLOv8训练自定义数据集
print("=" * 60)
print("YOLOv8 自定义数据集训练实战")
print("=" * 60)
 
yolo_training_code = '''
# 安装ultralytics
# pip install ultralytics
 
from ultralytics import YOLO
 
# 加载预训练模型
model = YOLO('yolov8n.pt')  # n=纳米版,还有s/m/l/x
 
# 训练 (COCO预训练模型微调)
results = model.train(
    data='path/to/your/data.yaml',  # 数据集配置文件
    epochs=100,
    imgsz=640,
    batch=16,
    device=0,  # GPU编号
    workers=8,
    project='runs/detect',  # 输出目录
    name='custom_train',
    
    # 数据增强
    mosaic=1.0,      # Mosaic增强
    mixup=0.1,       # MixUp增强
    copy_paste=0.1,   # Copy-paste增强
    
    # 优化器设置
    optimizer='AdamW',
    lr0=0.001,
    lrf=0.01,
    weight_decay=0.0005,
    
    # 正则化
    dropout=0.0,
    val=True,
    plots=True,
)
 
# 验证
metrics = model.val()
 
# 预测
results = model.predict(source='path/to/image.jpg', save=True)
 
# 导出模型
model.export(format='onnx')  # 导出为ONNX格式
 
# 导出格式支持: torchscript, onnx, saved_model, pb, tflite, edgetpu, etc.
'''
 
print(yolo_training_code)
 
# 数据集配置文件格式说明
dataset_yaml = '''
# data.yaml 示例
# 目录结构:
# dataset/
#   images/
#     train/
#     val/
#   labels/
#     train/
#     val/
 
# 数据集根目录
path: ./dataset
train: images/train
val: images/val
 
# 类别数量
nc: 2
 
# 类别名称
names:
  0: person
  1: car
'''
 
print("\n数据集配置文件格式:")
print(dataset_yaml)
 
# 标签格式说明
label_format = '''
# YOLO 标签格式 (.txt)
# 每行一个物体: class_id x_center y_center width height
# 所有坐标都是归一化的 (0-1)
 
# 示例 (2个物体):
# 0 0.5 0.5 0.3 0.4
# 1 0.2 0.3 0.1 0.2
 
# 坐标解释:
# - x_center, y_center: 物体中心点 (相对于图像宽高)
# - width, height: 包围盒宽高 (相对于图像宽高)
'''
 
print("\n标签文件格式:")
print(label_format)
 
# 完整训练示例
training_example = '''
# 完整训练脚本
from ultralytics import YOLO
import torch
 
def main():
    # 检查GPU
    print(f"GPU: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'CPU'}")
    
    # 加载模型 (从预训练权重开始)
    model = YOLO('yolov8m.pt')  # medium模型,精度和速度平衡
    
    # 开始训练
    results = model.train(
        data='custom_dataset.yaml',
        epochs=300,
        imgsz=640,
        batch=16,
        patience=50,      # 早停
        save=True,
        save_period=10,   # 每10个epoch保存一次
        cache=True,       # 缓存数据加速
        device=0,
        project='my_project',
        name='exp',
        exist_ok=True,
        pretrained=True,
        optimizer='SGD',
        lr0=0.01,
        lrf=0.01,
        momentum=0.937,
        weight_decay=0.0005,
        warmup_epochs=3,
        warmup_momentum=0.8,
        warmup_bias_lr=0.1,
        box=7.5,          # box loss权重
        cls=0.5,          # class loss权重
        dfl=1.5,          # dfl loss权重
        mosaic=1.0,
        mixup=0.1,
        copy_paste=0.1,
    )
    
    # 训练完成后的后处理
    print(f"最佳模型路径: {results.save_dir}/weights/best.pt")
    
    # 模型评估
    metrics = model.val()
    print(f"mAP50: {metrics.box.map50:.4f}")
    print(f"mAP50-95: {metrics.box.map:.4f}")
 
if __name__ == '__main__':
    main()
'''
 
print("\n完整训练脚本示例:")
print(training_example)

代码实战:U-Net医学图像分割

# U-Net 医学图像分割实战
print("=" * 60)
print("U-Net 医学图像分割实战")
print("=" * 60)
 
unet_training_code = '''
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
import numpy as np
import matplotlib.pyplot as plt
 
# 定义数据集
class MedicalSegmentationDataset(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):
        image = Image.open(self.image_paths[idx]).convert('RGB')
        mask = Image.open(self.mask_paths[idx]).convert('L')
        
        if self.transform:
            image = self.transform(image)
            mask = transforms.Resize((256, 256))(mask)
            mask = transforms.ToTensor()(mask)
            mask = (mask > 0.5).float()  # 二值化
        
        return image, mask
 
# 数据增强
train_transform = transforms.Compose([
    transforms.Resize((256, 256)),
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomRotation(10),
    transforms.ColorJitter(brightness=0.2, contrast=0.2),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], 
                        std=[0.229, 0.224, 0.225])
])
 
# Dice Loss + BCE Loss
class DiceBCELoss(nn.Module):
    def __init__(self, weight=None, size_average=True):
        super().__init__()
        self.bce = nn.BCEWithLogitsLoss()
    
    def forward(self, inputs, targets, smooth=1):
        # BCE Loss
        bce = self.bce(inputs, targets)
        
        # Dice Loss
        inputs = torch.sigmoid(inputs)
        inputs = inputs.view(-1)
        targets = targets.view(-1)
        
        intersection = (inputs * targets).sum()
        dice = (2. * intersection + smooth) / (inputs.sum() + targets.sum() + smooth)
        dice_loss = 1 - dice
        
        return bce + dice_loss
 
# IoU指标
def iou_score(pred, target, smooth=1):
    pred = torch.sigmoid(pred)
    pred = (pred > 0.5).float()
    
    intersection = (pred * target).sum()
    union = pred.sum() + target.sum() - intersection
    
    return (intersection + smooth) / (union + smooth)
 
# 训练循环
def train_unet(model, train_loader, val_loader, epochs=100, lr=1e-4):
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = model.to(device)
    
    criterion = DiceBCELoss()
    optimizer = optim.Adam(model.parameters(), lr=lr)
    scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', 
                                                      factor=0.5, patience=10)
    
    best_iou = 0
    
    for epoch in range(epochs):
        model.train()
        train_loss = 0
        train_iou = 0
        
        for images, masks in train_loader:
            images, masks = images.to(device), masks.to(device)
            
            optimizer.zero_grad()
            outputs = model(images)
            loss = criterion(outputs, masks)
            loss.backward()
            optimizer.step()
            
            train_loss += loss.item()
            train_iou += iou_score(outputs, masks).item()
        
        train_loss /= len(train_loader)
        train_iou /= len(train_loader)
        
        # 验证
        model.eval()
        val_loss = 0
        val_iou = 0
        
        with torch.no_grad():
            for images, masks in val_loader:
                images, masks = images.to(device), masks.to(device)
                outputs = model(images)
                loss = criterion(outputs, masks)
                val_loss += loss.item()
                val_iou += iou_score(outputs, masks).item()
        
        val_loss /= len(val_loader)
        val_iou /= len(val_loader)
        scheduler.step(val_loss)
        
        if val_iou > best_iou:
            best_iou = val_iou
            torch.save(model.state_dict(), 'best_unet.pth')
        
        if (epoch + 1) % 10 == 0:
            print(f"Epoch {epoch+1}/{epochs}")
            print(f"Train Loss: {train_loss:.4f}, Train IoU: {train_iou:.4f}")
            print(f"Val Loss: {val_loss:.4f}, Val IoU: {val_iou:.4f}")
    
    return model
 
# 使用segmentation_models_pytorch (推荐)
print("""
# 使用 segmentation_models_pytorch (更方便)
import segmentation_models_pytorch as smp
 
# 创建模型
model = smp.Unet(
    encoder_name="resnet34",
    encoder_weights="imagenet",
    in_channels=3,
    classes=1,
)
 
# 也支持其他架构
# model = smp.FPN("resnet34", in_channels=3, classes=1)
# model = smp.DeepLabV3Plus("efficientnet-b0", in_channels=3, classes=1)
 
# 训练
train_unet(model, train_loader, val_loader)
""")
 
# 使用segmentation_models_pytorch的完整示例
smp_example = '''
import segmentation_models_pytorch as smp
import torch
 
# 创建模型
model = smp.Unet(
    encoder_name="resnet34",
    encoder_weights="imagenet",
    in_channels=3,
    classes=1,
    activation=None  # 不要sigmoid,后面自己处理
)
 
# 使用预训练encoder
model = smp.Unet(
    encoder_name="efficientnet-b3",
    encoder_name_to_pretrain_model_path="efficientnet-b3-params",  # 可选
    encoder_weights="imagenet",
    in_channels=3,
    classes=3,  # 多类别分割
)
 
# 也支持其他架构
architectures = {
    'Unet': smp.Unet,
    'UnetPlusPlus': smp.UnetPlusPlus,
    'MAnet': smp.MAnet,
    'LinkNet': smp.LinkNet,
    'FPN': smp.FPN,
    'PSPNet': smp.PSPNet,
    'PAN': smp.PAN,
    'DeepLabV3': smp.DeepLabV3,
    'DeepLabV3Plus': smp.DeepLabV3Plus,
}
 
for name, arch in architectures.items():
    print(f"{name}: {arch.__name__}")
'''
print(smp_example)

总结

目标检测和图像分割是计算机视觉的核心任务。这篇文章涵盖了这两个领域的主要知识点。

目标检测从两阶段的R-CNN系列到单阶段的YOLO、SSD,再到最近的Transformer-based检测器(DETR、Swin-T),经历了快速发展。理解锚框机制、NMS、mAP这些核心概念,是掌握目标检测的关键。

图像分割分为语义分割、实例分割和全景分割。U-Net以其简洁有效的编解码+跳跃连接结构,特别适合医学图像等标注数据稀缺的场景。Mask R-CNN的RoIAlign和分割头设计,是实例分割的标杆。DeepLab系列的空洞卷积和ASPP,有效解决了多尺度问题。

实战部分展示了如何使用YOLOv8训练自定义数据集,以及如何使用U-Net和segmentation_models_pytorch进行医学图像分割。这些工具链已经非常成熟,可以快速落地应用。

最后提醒:目标检测和分割任务对标注数据的要求很高,质量好的标注数据往往比算法改进更重要。数据清洗和标注规范,是项目成功的关键。


本文为深度学习实战指南系列文章,主要涵盖目标检测与图像分割的核心知识点和实践技巧。