跳转至

第7章 目标检测

⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。

目标检测图

📚 章节概述

本章系统讲解目标检测的核心算法与前沿进展:从经典的 R-CNN 系列到 YOLO 全族谱,从 Anchor-Free 到 DETR Transformer 检测器,涵盖关键技术、小目标检测、3D 检测,并配有完整实战项目与面试高频题。

学习时间:7-10 天 难度等级:⭐⭐⭐⭐⭐ 前置知识:第 5-6 章(CNN 与图像分类)

🎯 学习目标

完成本章后,你将能够:

  • 理解目标检测的任务定义、评价指标(IoU / mAP / AP50 / AP75)
  • 掌握 Two-Stage(Faster R-CNN / FPN / Cascade R-CNN)检测器原理
  • 掌握 YOLO 全系列演进(v1 → v26/YOLO26)及 Ultralytics 代码实战
  • 理解 Anchor-Free(FCOS / CenterNet)与 DETR 系列检测器
  • 熟悉关键技术:数据增强 / 标签分配 / Loss 设计 / NMS 变体
  • 了解小目标检测与 3D 目标检测前沿
  • 完成 YOLOv8 自定义数据集训练与部署实战项目

7.1 目标检测概述

7.1.1 任务定义

目标检测(Object Detection):在图像中同时完成定位(Localization)和识别(Classification)。

对于每个检测到的目标,模型需要输出:

输出项 格式 说明
边界框 [x, y, w, h][x1, y1, x2, y2] 目标位置
类别标签 class_id 目标类别
置信度 confidence ∈ [0, 1] 预测可靠程度

与相关任务的区别: - 图像分类:一张图 → 一个标签 - 目标检测:一张图 → 多个 (框 + 标签 + 置信度) - 实例分割:在检测基础上加逐像素 mask

7.1.2 IoU(交并比)

IoU(Intersection over Union)是衡量预测框与真实框重合度的核心指标:

\[\text{IoU} = \frac{|B_{pred} \cap B_{gt}|}{|B_{pred} \cup B_{gt}|}\]
Python
import numpy as np

def calculate_iou(box1, box2):
    """计算两个边界框的IoU

    Args:
        box1: [x1, y1, x2, y2] 格式的边界框
        box2: [x1, y1, x2, y2] 格式的边界框
    Returns:
        iou: 交并比,范围 [0, 1]
    """
    # 计算交集区域
    x1 = max(box1[0], box2[0])
    y1 = max(box1[1], box2[1])
    x2 = min(box1[2], box2[2])
    y2 = min(box1[3], box2[3])

    intersection = 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 - intersection

    return intersection / union if union > 0 else 0.0

# 示例
pred_box = [50, 50, 200, 200]
gt_box   = [60, 40, 210, 190]
print(f"IoU = {calculate_iou(pred_box, gt_box):.4f}")
# IoU = 0.6806

7.1.3 mAP 评价指标体系

指标 定义 COCO 标准
AP 单类 Precision-Recall 曲线下面积 10 个 IoU 阈值取平均
AP50 IoU≥0.5 时的 AP PASCAL VOC 标准
AP75 IoU≥0.75 时的 AP 更严格的定位要求
mAP 所有类别 AP 的均值 COCO 主指标
AP_S 小目标(面积 < 32²)的 AP 小目标性能
AP_M 中目标(32² < 面积 < 96²)的 AP 中目标性能
AP_L 大目标(面积 > 96²)的 AP 大目标性能

📝 注意:此实现使用 VOC 2007 的 11 点插值法。COCO 使用 101 点插值并在多个 IoU 阈值 (0.50:0.05:0.95) 上取平均,结果通常更低且更严格。

Python
def compute_ap(recalls, precisions):
    """计算单类别 AP(VOC 2007 的 11 点插值法)

    Args:
        recalls: 召回率列表(按置信度降序排列)
        precisions: 精确率列表(与 recalls 一一对应)
    Returns:
        ap: Average Precision
    """
    # 11 点插值
    ap = 0.0
    for t in np.arange(0, 1.1, 0.1):
        # 取 recall >= t 时的最大 precision
        precs = [p for r, p in zip(recalls, precisions) if r >= t]  # zip按位置配对
        if precs:
            ap += max(precs) / 11.0
    return ap

def compute_map(all_aps):
    """计算 mAP = 所有类别 AP 的均值"""
    return np.mean(all_aps)

7.1.4 检测范式演进

Text Only
┌──────────────────────────────────────────────────────────────────────┐
│                     目标检测技术发展脉络                              │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  传统方法 ──► Two-Stage ──► One-Stage ──► Anchor-Free ──► Transformer│
│  (HOG+SVM)   (R-CNN)       (YOLO/SSD)   (FCOS)          (DETR)      │
│  2005         2014          2016          2019            2020        │
│                                                                      │
│  发展趋势: 精度↑  速度↑  结构简化  端到端                            │
└──────────────────────────────────────────────────────────────────────┘

7.1.5 主流数据集

数据集 类别数 训练集 验证集 特点
PASCAL VOC 20 5,011 (07) 4,952 (07) 经典基准
MS COCO 80 118K 5K 当前主流
Objects365 365 1.7M 80K 大规模预训练
LVIS 1,203 100K 20K 长尾分布
Open Images 600 1.7M 42K 超大规模
Python
# 使用 pycocotools 加载 COCO 数据集
from pycocotools.coco import COCO

coco = COCO('annotations/instances_val2017.json')
cat_ids = coco.getCatIds()         # 获取所有类别 id
img_ids = coco.getImgIds()         # 获取所有图像 id
print(f"COCO 类别数: {len(cat_ids)}, 图像数: {len(img_ids)}")

# 查看某张图像的标注
img_info = coco.loadImgs(img_ids[0])[0]
ann_ids = coco.getAnnIds(imgIds=img_info['id'])
anns = coco.loadAnns(ann_ids)
print(f"图像 {img_info['file_name']}{len(anns)} 个标注")

📝 面试考点:IoU 计算是手撕代码常考题;mAP 的 COCO 计算方式(10 个 IoU 阈值 0.5:0.05:0.95 取均值);AP_S/AP_M/AP_L 的面积阈值。


7.2 两阶段检测器(Two-Stage Detectors)

7.2.1 R-CNN(2014)

核心思路:先生成候选区域(Region Proposals),再对每个区域分类与回归。

Text Only
输入图像 → Selective Search (~2000 候选框)
         → 逐个裁剪 & resize 至 227×227
         → CNN 提取特征(AlexNet)
         → SVM 分类 + 边界框回归

局限: - 每张图需对 ~2000 个候选区域分别前向传播 → 极慢(GPU 上 ~47s / 张) - 多阶段训练:CNN → SVM → BBox Regressor 分别训练 - 特征无法共享

7.2.2 Fast R-CNN(2015)

关键改进: 1. 整图卷积:先对整张图提取特征图,再从特征图上裁剪候选区域 → 特征共享 2. RoI Pooling:将不同大小的候选区域映射为固定大小特征 3. 多任务 Loss:分类 + 回归联合训练

\[L = L_{cls}(p, u) + \lambda \cdot [u \geq 1] \cdot L_{loc}(t^u, v)\]
Python
import torch
import torch.nn as nn
import torchvision

class FastRCNN(nn.Module):  # 继承nn.Module定义网络层
    """Fast R-CNN 简化实现"""
    def __init__(self, num_classes=21):
        super().__init__()  # super()调用父类方法
        # Backbone: 共享卷积特征
        backbone = torchvision.models.vgg16(weights='DEFAULT')
        self.features = backbone.features       # 共享卷积层

        # RoI Pooling: 将任意大小区域转为 7×7
        self.roi_pool = torchvision.ops.RoIPool(
            output_size=(7, 7),
            spatial_scale=1.0 / 16  # VGG16 下采样 16 倍
        )

        # 分类 + 回归头
        self.classifier = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
        )
        self.cls_head = nn.Linear(4096, num_classes)       # 类别
        self.reg_head = nn.Linear(4096, num_classes * 4)   # 边界框回归

    def forward(self, images, rois):
        """
        Args:
            images: (B, 3, H, W)
            rois: List[Tensor], 每个 Tensor shape = (N_i, 5),
                  格式 [batch_idx, x1, y1, x2, y2]
        """
        feat = self.features(images)                       # 共享特征
        pooled = self.roi_pool(feat, rois)                 # (N, 512, 7, 7)
        pooled = pooled.flatten(1)                         # (N, 512*7*7)
        fc = self.classifier(pooled)                       # (N, 4096)
        cls_score = self.cls_head(fc)                      # (N, num_classes)
        bbox_pred = self.reg_head(fc)                      # (N, num_classes*4)
        return cls_score, bbox_pred

7.2.3 Faster R-CNN(2015)

核心创新:用 RPN(Region Proposal Network) 替代 Selective Search,实现端到端训练。

Text Only
输入图像 → Backbone (ResNet/VGG) → 共享特征图
                                     ├→ RPN → 候选区域
                                     └→ RoI Pooling → 分类 + 回归

RPN 设计: - 在特征图每个位置放置 k 个 anchor(不同尺度 × 不同比例) - 典型设置:3 个尺度 × 3 个比例 = 9 个 anchor / 位置 - 输出:前景/背景分数 + 边界框偏移

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

class RPN(nn.Module):
    """Region Proposal Network"""
    def __init__(self, in_channels=512, num_anchors=9):
        super().__init__()
        # 3×3 卷积提取局部特征
        self.conv = nn.Conv2d(in_channels, 512, 3, padding=1)
        # 分类头: 每个 anchor 的前景/背景二分类
        self.cls_head = nn.Conv2d(512, num_anchors * 2, 1)
        # 回归头: 每个 anchor 的边界框偏移 (dx, dy, dw, dh)
        self.reg_head = nn.Conv2d(512, num_anchors * 4, 1)

        # 权重初始化
        for layer in [self.conv, self.cls_head, self.reg_head]:
            nn.init.normal_(layer.weight, std=0.01)
            nn.init.constant_(layer.bias, 0)

    def forward(self, feature_map):
        """
        Args:
            feature_map: (B, C, H, W) 来自 backbone 的特征图
        Returns:
            cls_score: (B, num_anchors*2, H, W) 前景/背景分数
            bbox_pred: (B, num_anchors*4, H, W) 偏移量
        """
        x = F.relu(self.conv(feature_map))  # F.xxx PyTorch函数式API
        cls_score = self.cls_head(x)   # (B, 18, H, W)
        bbox_pred = self.reg_head(x)   # (B, 36, H, W)
        return cls_score, bbox_pred

def generate_anchors(feature_h, feature_w, stride=16):
    """生成所有 anchor 坐标

    Args:
        feature_h, feature_w: 特征图尺寸
        stride: 下采样步长
    Returns:
        anchors: (H*W*9, 4) 格式 [x1, y1, x2, y2]
    """
    scales = [128, 256, 512]          # anchor 尺度
    ratios = [0.5, 1.0, 2.0]         # 宽高比

    anchors = []
    for i in range(feature_h):
        for j in range(feature_w):
            cx = (j + 0.5) * stride   # anchor 中心 x
            cy = (i + 0.5) * stride   # anchor 中心 y
            for s in scales:
                for r in ratios:
                    w = s * (r ** 0.5)
                    h = s / (r ** 0.5)
                    anchors.append([
                        cx - w / 2, cy - h / 2,
                        cx + w / 2, cy + h / 2
                    ])
    return torch.tensor(anchors, dtype=torch.float32)

7.2.4 RoI Align(Mask R-CNN, 2017)

RoI Pooling 的量化操作导致特征错位,RoI Align 使用双线性插值消除量化误差:

Python
import torchvision.ops as ops

# RoI Pooling: 有量化误差(坐标取整)
roi_pool = ops.RoIPool(output_size=(7, 7), spatial_scale=1/16)

# RoI Align: 双线性插值,无量化误差,更精确
roi_align = ops.RoIAlign(
    output_size=(7, 7),
    spatial_scale=1.0 / 16,
    sampling_ratio=2          # 每个 bin 采样 2×2 个点
)

# 使用示例
feature_map = torch.randn(1, 256, 50, 50)   # backbone 输出
rois = torch.tensor([[0, 10.6, 20.3, 100.7, 150.2]])  # [batch_idx, x1, y1, x2, y2]

pooled = roi_align(feature_map, rois)  # (1, 256, 7, 7)

7.2.5 FPN(Feature Pyramid Network, 2017)

FPN 构建自顶向下的多尺度特征金字塔,让每个尺度都拥有丰富语义信息:

Text Only
                 ┌────────┐
C5 (1/32) ──────►│ P5     │
    ↑ lateral    │ 256ch  │
C4 (1/16) ──►+──►│ P4     │  ← 自顶向下路径(上采样 + 横向连接)
    ↑ lateral    │ 256ch  │
C3 (1/8)  ──►+──►│ P3     │
    ↑ lateral    │ 256ch  │
C2 (1/4)  ──►+──►│ P2     │
Python
import torch
import torch.nn as nn
import torch.nn.functional as F

class FPN(nn.Module):
    """Feature Pyramid Network"""
    def __init__(self, in_channels_list, out_channels=256):
        """
        Args:
            in_channels_list: backbone 各层输出通道数,如 [256, 512, 1024, 2048]
            out_channels: FPN 统一输出通道数
        """
        super().__init__()
        # 横向连接: 1×1 卷积统一通道数
        self.lateral_convs = nn.ModuleList([
            nn.Conv2d(in_ch, out_channels, 1) for in_ch in in_channels_list
        ])
        # 输出卷积: 3×3 卷积消除上采样混叠
        self.output_convs = nn.ModuleList([
            nn.Conv2d(out_channels, out_channels, 3, padding=1)
            for _ in in_channels_list
        ])

    def forward(self, features):
        """
        Args:
            features: [C2, C3, C4, C5] backbone 多尺度特征
        Returns:
            fpn_features: [P2, P3, P4, P5]
        """
        # 先做横向连接
        laterals = [conv(f) for conv, f in zip(self.lateral_convs, features)]

        # 自顶向下融合
        for i in range(len(laterals) - 2, -1, -1):
            up = F.interpolate(laterals[i + 1], size=laterals[i].shape[2:], mode='nearest')
            laterals[i] = laterals[i] + up

        # 输出卷积
        outputs = [conv(lat) for conv, lat in zip(self.output_convs, laterals)]
        return outputs

7.2.6 Cascade R-CNN(2018)

核心思想:多级级联检测头,逐步提高 IoU 阈值:

Text Only
RPN → RoI (IoU=0.5) → Head1 → 精修框 → RoI (IoU=0.6) → Head2 → 精修框 → RoI (IoU=0.7) → Head3
  • 每一级使用更严格的正样本阈值,逐步逼近精确定位
  • 解决了单一 IoU 阈值的"欠拟合/过拟合"矛盾
  • COCO 上 AP 提升 ~2-4 个点

📝 面试考点:Faster R-CNN 整体流程(Backbone → RPN → NMS → RoI Align → Head);RPN 的 anchor 设计(尺度 × 比例);RoI Pooling vs RoI Align 的区别(量化误差);FPN 自顶向下融合方式;Cascade R-CNN 多级级联思想。


7.3 YOLO 系列完整演进

📌 版本说明:YOLO系列更新频繁,截至2026年2月,YOLO最新版本为YOLO26(2025年9月发布)。本节内容基于当时可获得的最新信息,新版本发布后请参考官方文档获取最新特性。

7.3.1 YOLOv1(2016)— "You Only Look Once"

核心思想:将检测视为回归问题,单次前向传播完成。

  • 将图像划分为 \(S \times S\) 网格(默认 7×7)
  • 每个网格预测 \(B\) 个边界框 + \(C\) 个类别概率
  • 输出张量:\(S \times S \times (B \times 5 + C)\),即 \(7 \times 7 \times 30\)
\[P(\text{Class}_i | \text{Object}) \times P(\text{Object}) \times \text{IoU}_{pred}^{truth} = P(\text{Class}_i) \times \text{IoU}_{pred}^{truth}\]

局限:每个网格只预测 2 个框 → 密集小目标效果差。

7.3.2 YOLOv2 / YOLO9000(2017)

改进策略 效果
Batch Normalization 去掉 Dropout,mAP +2%
高分辨率预训练 先 448×448 ImageNet 微调
Anchor Boxes 使用锚框替代直接预测
K-means 聚类 anchor 数据驱动 anchor 尺寸
多尺度训练 每 10 个 batch 随机切换输入尺寸
Passthrough 层 浅层细粒度特征拼接
Darknet-19 更高效的骨干网络

7.3.3 YOLOv3(2018)

  • Darknet-53 骨干(残差连接)
  • 多尺度预测:3 个尺度(类似 FPN)
  • 每个尺度 3 个 anchor → 共 9 个 anchor
  • 多标签分类(使用 sigmoid 替代 softmax)

7.3.4 YOLOv4(2020)

YOLOv4 是技巧集大成者,提出 Bag of FreebiesBag of Specials

类别 技术
Backbone CSPDarknet53
Neck SPP + PANet (Path Aggregation)
BoF(训练技巧) Mosaic 增强、CutMix、DropBlock、CIoU Loss、Label Smoothing
BoS(推理技巧) Mish 激活、CSP 连接、SAM 注意力、DIoU-NMS

7.3.5 YOLOv5(2020, Ultralytics)

特点:工程化做到极致的非论文版本

  • PyTorch 原生实现,Ultralytics 开源
  • 自动 anchor 计算、自动混合精度训练
  • 模型缩放:n / s / m / l / x 五个尺寸
  • 部署友好:ONNX / TensorRT / CoreML / TFLite 导出

7.3.6 YOLOv8(2023, Ultralytics)

架构革新

组件 变化
Head Anchor-Free 解耦头(分类 + 回归分离)
标签分配 TAL(Task-Aligned Assigner)
Loss DFL(Distribution Focal Loss)+ CIoU
Backbone 改进 CSP,C2f 模块
支持任务 检测 / 分割 / 分类 / 姿态估计 / OBB
Python
# YOLOv8 使用示例(Ultralytics CLI & Python)
from ultralytics import YOLO

# --------- 推理 ---------
model = YOLO('yolov8n.pt')            # 加载预训练模型(nano 版)
results = model('bus.jpg')             # 推理
results[0].show()                      # 可视化

# --------- 训练 ---------
model = YOLO('yolov8s.pt')            # small 版本
model.train(
    data='coco128.yaml',              # 数据集配置
    epochs=100,
    imgsz=640,
    batch=16,
    device='0',                       # GPU
)

# --------- 导出 ---------
model.export(format='onnx')           # 导出 ONNX
model.export(format='engine')         # 导出 TensorRT

7.3.7 YOLOv10(2024)—— NMS-Free

  • 端到端:去除 NMS 后处理,使用 one-to-one 匹配 + one-to-many 匹配双头
  • 一致性双重分配:训练用 one-to-many 加速收敛,推理用 one-to-one 去除 NMS
  • 效率优化:空间-通道解耦下采样、rank-guided block 设计
  • 相比 YOLOv8 延迟降低 46%

7.3.8 YOLOv11(2024, Ultralytics)

  • 更高效的 C3k2 模块(替代 C2f)
  • SPPF → C2PSA(跨阶段部分空间注意力)
  • 参数更少,精度更高
  • 支持检测 / 分割 / 姿态 / OBB / 分类五大任务

7.3.9 YOLO26(2025, Ultralytics)— 当前最新版本

🆕 最新版本:YOLO26于2025年9月正式发布,是当前YOLO系列的最新版本。

核心创新: - 全新的Transformer-CNN混合架构 - 增强的多尺度特征融合机制 - 更高效的自适应锚框策略 - 支持更多视觉任务(检测/分割/姿态/OBB/分类/跟踪) - 在COCO数据集上达到新的SOTA性能

主要改进: - 推理速度相比v11提升约25% - 参数效率进一步优化 - 小目标检测能力显著增强 - 更好的跨域泛化性能

7.3.10 YOLO 系列演进对比表

版本 年份 Backbone Neck Head Anchor NMS COCO mAP 关键创新
v1 2016 GoogleNet 变体 - 全连接 需要 63.4 (VOC) 单阶段回归
v2 2017 Darknet-19 Passthrough Conv K-means 先验 需要 48.1 BN+多尺度训练
v3 2018 Darknet-53 FPN-like Conv 9 anchors 需要 33.0 多尺度预测
v4 2020 CSPDarknet53 SPP+PAN Conv 9 anchors 需要 43.5 BoF+BoS+Mosaic
v5 2020 CSPDarknet PAN Conv Auto-anchor 需要 50.7(x) 工程化+多尺寸
v8 2023 CSP+C2f PAN 解耦头 Anchor-Free 需要 53.9(x) TAL+DFL
v10 2024 CSP 改进 PAN 双头 Anchor-Free 不需要 54.4(x) NMS-Free
v11 2024 C3k2 C2PSA 解耦头 Anchor-Free 需要 54.7(x) 高效注意力
v26 2025 Transformer-CNN混合 增强PAN 解耦头 自适应Anchor-Free 需要 56.2(x) 混合架构+SOTA

📝 面试考点:YOLO v1 的 grid 机制与局限性;v3 多尺度检测原理;v4 Mosaic/CIoU 等训练技巧;v5 vs v8 架构区别(anchor-based → anchor-free);v10 如何去除 NMS;YOLO 系列精度-速度 trade-off。


7.4 Anchor-Free 检测器

7.4.1 为什么要 Anchor-Free?

Anchor-Based 的缺陷: 1. 超参数敏感:anchor 的尺度、比例需要精心设计或 K-means 聚类 2. 正负样本不平衡:大量负样本 anchor 主导训练 3. 计算冗余:大量 anchor 最终被 NMS 过滤

7.4.2 FCOS(Fully Convolutional One-Stage, 2019)

核心思想:逐像素预测,每个特征点直接回归到 GT 框的四边距离。

Text Only
特征点 (x, y) → 预测 (l, t, r, b) + class + centerness

l = x - x0    (到左边距离)
t = y - y0    (到上边距离)
r = x1 - x    (到右边距离)
b = y1 - y    (到下边距离)

Center-ness:抑制远离目标中心的低质量框

\[\text{centerness}^* = \sqrt{\frac{\min(l, r)}{\max(l, r)} \times \frac{\min(t, b)}{\max(t, b)}}\]
Python
import torch
import torch.nn as nn

class FCOSHead(nn.Module):
    """FCOS 检测头(简化版)"""
    def __init__(self, in_channels=256, num_classes=80):
        super().__init__()
        # 分类分支
        cls_tower = []
        for _ in range(4):   # 4 层卷积
            cls_tower.append(nn.Conv2d(in_channels, in_channels, 3, padding=1))
            cls_tower.append(nn.GroupNorm(32, in_channels))
            cls_tower.append(nn.ReLU())
        self.cls_tower = nn.Sequential(*cls_tower)
        self.cls_logits = nn.Conv2d(in_channels, num_classes, 3, padding=1)

        # 回归分支
        reg_tower = []
        for _ in range(4):
            reg_tower.append(nn.Conv2d(in_channels, in_channels, 3, padding=1))
            reg_tower.append(nn.GroupNorm(32, in_channels))
            reg_tower.append(nn.ReLU())
        self.reg_tower = nn.Sequential(*reg_tower)
        self.bbox_pred = nn.Conv2d(in_channels, 4, 3, padding=1)  # (l, t, r, b)

        # center-ness 分支
        self.centerness = nn.Conv2d(in_channels, 1, 3, padding=1)

    def forward(self, x):
        cls_feat = self.cls_tower(x)
        reg_feat = self.reg_tower(x)

        cls_score = self.cls_logits(cls_feat)        # (B, C, H, W)
        bbox = torch.exp(self.bbox_pred(reg_feat))   # (B, 4, H, W) 正值
        ctr = self.centerness(cls_feat)               # (B, 1, H, W)
        return cls_score, bbox, ctr

7.4.3 CenterNet(2019)

核心思想:将目标建模为中心点(关键点检测思路)。

  • 使用 热力图(Heatmap) 预测中心点位置
  • 从中心点回归尺寸 \((w, h)\) 和偏移量
  • 无需 NMS:每个类别取热力图局部极大值(peak)
Python
class CenterNetHead(nn.Module):
    """CenterNet 检测头"""
    def __init__(self, in_channels=64, num_classes=80):
        super().__init__()
        # 热力图分支: 预测中心点
        self.heatmap = nn.Sequential(
            nn.Conv2d(in_channels, in_channels, 3, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels, num_classes, 1),
        )
        # 尺寸分支: 预测宽高
        self.wh = nn.Sequential(
            nn.Conv2d(in_channels, in_channels, 3, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels, 2, 1),   # (w, h)
        )
        # 偏移分支: 下采样导致的中心偏移
        self.offset = nn.Sequential(
            nn.Conv2d(in_channels, in_channels, 3, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels, 2, 1),   # (offset_x, offset_y)
        )

    def forward(self, x):
        hm = torch.sigmoid(self.heatmap(x))   # 热力图
        wh = self.wh(x)                        # 宽高
        off = self.offset(x)                   # 偏移
        return hm, wh, off

7.4.4 CornerNet(2018)

  • 将目标建模为左上角 + 右下角两个关键点
  • 使用 Corner Pooling 聚合边界信息
  • 使用 Embedding 向量匹配同一目标的两个角点

7.4.5 Anchor-Based vs Anchor-Free 对比

维度 Anchor-Based Anchor-Free
代表模型 Faster R-CNN, YOLOv3-v5 FCOS, CenterNet, YOLOv8
先验框 需要预定义 不需要
超参数 尺度/比例/数量 较少
正负样本分配 IoU 阈值 点在框内/center sampling
后处理 NMS NMS 或直接取 peak
泛化性 依赖 anchor 设计 更好
当前趋势 逐渐被取代 主流

📝 面试考点:FCOS 的 centerness 分支作用;CenterNet 如何避免 NMS;Anchor-Free 如何解决正负样本分配问题;Anchor-Based 与 Anchor-Free 优缺点。


7.5 DETR 系列(Transformer 检测器)

7.5.1 DETR(2020)— Detection Transformer

革命性设计:首次将 Transformer 引入目标检测,实现真正端到端——无需 anchor、无需 NMS。

Text Only
输入图像 → CNN Backbone → Transformer Encoder → Transformer Decoder → FFN → 预测
                                            Object Queries (可学习)

关键组件

  1. Object Queries\(N\) 个可学习的查询向量(通常 \(N=100\)),每个负责产生一个预测
  2. 二分图匹配(Hungarian Matching):将预测与 GT 一一最优匹配
  3. 集合预测 Loss:基于匹配结果计算分类 + L1 + GIoU loss
Python
import torch
import torch.nn as nn
from scipy.optimize import linear_sum_assignment

class HungarianMatcher(nn.Module):
    """匈牙利匹配器: 预测与GT的最优一一匹配"""
    def __init__(self, cost_class=1.0, cost_bbox=5.0, cost_giou=2.0):
        super().__init__()
        self.cost_class = cost_class
        self.cost_bbox = cost_bbox
        self.cost_giou = cost_giou

    @torch.no_grad()  # 禁用梯度计算,节省内存
    def forward(self, outputs, targets):
        """
        Args:
            outputs: {"pred_logits": (B, N, C), "pred_boxes": (B, N, 4)}
            targets: [{"labels": (M,), "boxes": (M, 4)}, ...] per image
        Returns:
            indices: [(pred_idx, gt_idx), ...] 每张图的匹配结果
        """
        B, N = outputs["pred_logits"].shape[:2]  # 切片操作,取前n个元素

        # 拼接 batch
        out_prob = outputs["pred_logits"].flatten(0, 1).softmax(-1)  # (B*N, C)
        out_bbox = outputs["pred_boxes"].flatten(0, 1)               # (B*N, 4)

        tgt_ids = torch.cat([t["labels"] for t in targets])  # torch.cat沿已有维度拼接张量
        tgt_bbox = torch.cat([t["boxes"] for t in targets])

        # 计算代价矩阵
        cost_class = -out_prob[:, tgt_ids]                          # 分类代价
        cost_bbox = torch.cdist(out_bbox, tgt_bbox, p=1)           # L1 代价
        cost_giou = -self._generalized_iou(out_bbox, tgt_bbox)     # GIoU 代价

        C = (self.cost_class * cost_class +
             self.cost_bbox * cost_bbox +
             self.cost_giou * cost_giou)
        C = C.view(B, N, -1).cpu()  # 重塑张量形状

        # 对每张图独立做匈牙利匹配
        sizes = [len(t["labels"]) for t in targets]
        indices = []
        for i, c in enumerate(C.split(sizes, -1)):  # enumerate同时获取索引和元素
            row_ind, col_ind = linear_sum_assignment(c[i])
            indices.append((
                torch.as_tensor(row_ind, dtype=torch.int64),
                torch.as_tensor(col_ind, dtype=torch.int64)
            ))
        return indices

    def _generalized_iou(self, boxes1, boxes2):
        """计算 Generalized IoU(简化版)"""
        # 简化实现:此处使用IoU替代GIoU。完整GIoU = IoU - (C_area - union) / C_area,其中C为最小外接矩形面积
        # 实际实现需要处理 cxcywh → xyxy 转换
        return torch.zeros(len(boxes1), len(boxes2))  # placeholder

class SimpleDETR(nn.Module):
    """DETR 简化版本(展示核心架构)"""
    def __init__(self, num_classes=91, num_queries=100, d_model=256):
        super().__init__()
        # Backbone: ResNet50(取最后一层特征)
        import torchvision
        backbone = torchvision.models.resnet50(weights='DEFAULT')
        self.backbone = nn.Sequential(*list(backbone.children())[:-2])

        # 降维
        self.input_proj = nn.Conv2d(2048, d_model, 1)

        # 位置编码
        # 注意:此处位置编码固定大小,实际DETR使用可学习或正弦位置编码,可适配不同输入尺寸
        self.pos_embed = nn.Parameter(torch.randn(1, d_model, 25, 25))

        # Transformer
        self.transformer = nn.Transformer(
            d_model=d_model, nhead=8,
            num_encoder_layers=6, num_decoder_layers=6,
            dim_feedforward=2048, dropout=0.1,
            batch_first=True
        )

        # Object Queries: 可学习的查询向量
        self.query_embed = nn.Embedding(num_queries, d_model)

        # 输出头
        self.class_head = nn.Linear(d_model, num_classes + 1)  # +1 = "no object"
        self.bbox_head = nn.Sequential(
            nn.Linear(d_model, d_model),
            nn.ReLU(),
            nn.Linear(d_model, d_model),
            nn.ReLU(),
            nn.Linear(d_model, 4),    # (cx, cy, w, h) 归一化坐标
            nn.Sigmoid()
        )

    def forward(self, x):
        # Backbone 特征
        feat = self.backbone(x)                     # (B, 2048, H/32, W/32)
        feat = self.input_proj(feat)                 # (B, 256, H', W')
        B, C, H, W = feat.shape

        # 展平 + 位置编码
        pos = self.pos_embed[:, :, :H, :W].flatten(2).permute(0, 2, 1)
        src = feat.flatten(2).permute(0, 2, 1) + pos  # (B, H'*W', 256)

        # Transformer
        queries = self.query_embed.weight.unsqueeze(0).repeat(B, 1, 1)  # (B, N, 256)  # unsqueeze增加一个维度
        hs = self.transformer(src, queries)           # (B, N, 256)

        # 输出
        cls = self.class_head(hs)                     # (B, N, num_classes+1)
        bbox = self.bbox_head(hs)                     # (B, N, 4)
        return {"pred_logits": cls, "pred_boxes": bbox}

7.5.2 Deformable DETR(2021)

DETR 的问题:收敛慢(需 500 epochs)、小目标差。

Deformable Attention: - 不做全局 attention → 每个 query 只关注少量采样点 - 采样位置可学习(类似 deformable conv) - 多尺度特征融合

\[\text{DeformAttn}(z_q, p_q, x) = \sum_{m=1}^{M} W_m \left[\sum_{k=1}^{K} A_{mqk} \cdot x(p_q + \Delta p_{mqk})\right]\]
  • 收敛速度提升 10×(50 epochs 即可)
  • 支持多尺度特征

7.5.3 DINO(2022)

DETR with Improved deNoising anchOr boxes

  • 对比去噪训练:向 GT 添加噪声作为正样本辅助训练
  • 混合查询:content queries + position queries 分离
  • Look Forward Twice:decoder 层间参数更新策略
  • COCO AP 达到 63.3(SwinL backbone)

7.5.4 RT-DETR(2023, 百度)

Real-Time DETR:首个实时 Transformer 检测器

  • 高效混合编码器:减少 encoder 计算量
  • IoU-aware 查询选择:选择高质量 query 初始化 decoder
  • 灵活速度调节:通过调整 decoder 层数控制精度-速度
  • T4 GPU 上达到 114 FPS(RT-DETR-R50)
Python
# RT-DETR 使用 (Ultralytics 集成)
from ultralytics import RTDETR

model = RTDETR('rtdetr-l.pt')          # 加载预训练
results = model('bus.jpg')             # 推理
model.train(data='coco.yaml', epochs=100)  # 训练

7.5.5 DETR 系列发展对比

模型 年份 收敛 epochs COCO AP 特点
DETR 2020 500 42.0 开创性端到端
Deformable DETR 2021 50 46.2 可变形注意力
DAB-DETR 2022 50 45.7 动态 anchor boxes
DN-DETR 2022 50 48.6 去噪训练
DINO 2022 36 63.3 去噪 + 对比
RT-DETR 2023 72 54.8 实时
Co-DETR 2023 36 66.0 协同训练

📝 面试考点:DETR 的 Hungarian Matching 原理(二分图最优匹配);为什么 DETR 收敛慢(全局 attention + 稀疏监督);Deformable DETR 如何加速收敛;DETR vs YOLO 的设计哲学差异;RT-DETR 实时化策略。


7.6 关键技术

7.6.1 数据增强

Mosaic 增强(YOLOv4 提出)

4 张图像 拼接为 1 张,丰富上下文 & 隐式增大 batch size:

Python
import cv2
import numpy as np
import random

def mosaic_augmentation(images, labels_list, img_size=640):
    """Mosaic 数据增强: 4 张图拼接

    Args:
        images: 4 张图像列表
        labels_list: 4 个标注列表,每个元素 shape=(N, 5) [cls, cx, cy, w, h] 归一化
        img_size: 输出图像尺寸
    Returns:
        mosaic_img: 拼接后图像
        mosaic_labels: 合并后标注
    """
    s = img_size
    # 随机选择拼接中心点
    cx = int(random.uniform(s * 0.25, s * 0.75))
    cy = int(random.uniform(s * 0.25, s * 0.75))

    mosaic_img = np.zeros((s, s, 3), dtype=np.uint8)
    mosaic_labels = []

    # 4 个位置: 左上, 右上, 左下, 右下
    placements = [
        (0, 0, cx, cy),         # 左上
        (cx, 0, s, cy),         # 右上
        (0, cy, cx, s),         # 左下
        (cx, cy, s, s),         # 右下
    ]

    for i, (x1, y1, x2, y2) in enumerate(placements):
        img = images[i]
        h, w = img.shape[:2]

        # resize 到目标区域大小
        pw, ph = x2 - x1, y2 - y1
        resized = cv2.resize(img, (pw, ph))
        mosaic_img[y1:y2, x1:x2] = resized

        # 转换标注坐标
        if len(labels_list[i]) > 0:
            labels = labels_list[i].copy()
            labels[:, 1] = labels[:, 1] * pw / s + x1 / s  # cx
            labels[:, 2] = labels[:, 2] * ph / s + y1 / s  # cy
            labels[:, 3] = labels[:, 3] * pw / s            # w
            labels[:, 4] = labels[:, 4] * ph / s            # h
            mosaic_labels.append(labels)

    if mosaic_labels:
        mosaic_labels = np.concatenate(mosaic_labels, axis=0)
    else:
        mosaic_labels = np.zeros((0, 5))

    return mosaic_img, mosaic_labels

MixUp 增强

两张图加权叠加:\(\text{img} = \lambda \cdot \text{img}_1 + (1 - \lambda) \cdot \text{img}_2\)

Python
def mixup_augmentation(img1, labels1, img2, labels2, alpha=1.5):
    """MixUp 数据增强"""
    lam = np.random.beta(alpha, alpha)

    # 确保尺寸相同
    h = max(img1.shape[0], img2.shape[0])
    w = max(img1.shape[1], img2.shape[1])
    img1 = cv2.resize(img1, (w, h))
    img2 = cv2.resize(img2, (w, h))

    mixed_img = (lam * img1 + (1 - lam) * img2).astype(np.uint8)
    mixed_labels = np.concatenate([labels1, labels2], axis=0)

    return mixed_img, mixed_labels

CopyPaste 增强

从一张图中分割出目标,粘贴到另一张图上,标签相应合并。常用于实例分割任务,也可用于检测。

7.6.2 标签分配策略

ATSS(Adaptive Training Sample Selection, 2020)

核心:根据统计信息自适应选择正样本阈值。

  1. 对每个 GT,在每个 FPN 层选择 top-k(如 k=9)最近 anchor
  2. 计算这些 anchor 与 GT 的 IoU
  3. 以 IoU 的均值 + 标准差作为正样本阈值
  4. IoU 大于阈值且中心在 GT 框内的 anchor 为正样本

SimOTA(YOLOX, 2021)

核心:简化的 OTA(Optimal Transport Assignment)。

  1. 计算每个预测与 GT 的代价矩阵(cls cost + reg cost)
  2. 对每个 GT 动态确定正样本数量 \(k\)(根据 IoU 前 k 个之和取整)
  3. 选择代价最低的 \(k\) 个预测作为正样本

TAL(Task-Aligned Assignment, YOLOv8)

同时考虑分类和定位的对齐程度:

\[t = s^\alpha \cdot u^\beta\]

其中 \(s\) 是分类分数,\(u\) 是 IoU,\(\alpha = 0.5, \beta = 6.0\)。选择 alignment metric 最大的 top-k 作为正样本。

7.6.3 Loss 函数

Focal Loss(2017, RetinaNet)

解决 正负样本极度不平衡 问题:

\[\text{FL}(p_t) = -\alpha_t (1 - p_t)^\gamma \log(p_t)\]
  • \((1 - p_t)^\gamma\) 降低已分类样本的权重 → 聚焦难分样本
  • 默认 \(\gamma = 2, \alpha = 0.25\)
Python
import torch
import torch.nn.functional as F

def focal_loss(pred, target, alpha=0.25, gamma=2.0):
    """Focal Loss 实现

    Args:
        pred: (N, C) 预测 logits
        target: (N,) 目标类别
        alpha: 平衡因子
        gamma: 聚焦参数
    """
    ce_loss = F.cross_entropy(pred, target, reduction='none')  # F.cross_entropy PyTorch函数式交叉熵损失
    p_t = torch.exp(-ce_loss)              # 预测正确的概率
    focal_weight = alpha * (1 - p_t) ** gamma
    loss = focal_weight * ce_loss
    return loss.mean()

IoU Loss 系列

Loss 公式核心 优点
IoU Loss \(1 - \text{IoU}\) 尺度不变
GIoU Loss \(1 - \text{GIoU}\),考虑最小外接框 无交集时仍有梯度
DIoU Loss \(1 - \text{IoU} + \frac{d^2}{c^2}\)\(d\)=中心距,\(c\)=对角线 加速收敛
CIoU Loss DIoU + 宽高比惩罚项 \(\alpha v\) 最常用
Python
import torch

def ciou_loss(pred_boxes, target_boxes):
    """CIoU Loss 实现

    Args:
        pred_boxes: (N, 4) 格式 [x1, y1, x2, y2]
        target_boxes: (N, 4) 格式 [x1, y1, x2, y2]
    Returns:
        loss: CIoU Loss 均值
    """
    # 交集
    inter_x1 = torch.max(pred_boxes[:, 0], target_boxes[:, 0])
    inter_y1 = torch.max(pred_boxes[:, 1], target_boxes[:, 1])
    inter_x2 = torch.min(pred_boxes[:, 2], target_boxes[:, 2])
    inter_y2 = torch.min(pred_boxes[:, 3], target_boxes[:, 3])
    inter_area = torch.clamp(inter_x2 - inter_x1, min=0) * \
                 torch.clamp(inter_y2 - inter_y1, min=0)

    # 各自面积
    area1 = (pred_boxes[:, 2] - pred_boxes[:, 0]) * (pred_boxes[:, 3] - pred_boxes[:, 1])
    area2 = (target_boxes[:, 2] - target_boxes[:, 0]) * (target_boxes[:, 3] - target_boxes[:, 1])
    union = area1 + area2 - inter_area
    iou = inter_area / (union + 1e-7)

    # 最小外接矩形对角线
    enclose_x1 = torch.min(pred_boxes[:, 0], target_boxes[:, 0])
    enclose_y1 = torch.min(pred_boxes[:, 1], target_boxes[:, 1])
    enclose_x2 = torch.max(pred_boxes[:, 2], target_boxes[:, 2])
    enclose_y2 = torch.max(pred_boxes[:, 3], target_boxes[:, 3])
    c2 = (enclose_x2 - enclose_x1) ** 2 + (enclose_y2 - enclose_y1) ** 2 + 1e-7

    # 中心点距离
    pred_cx = (pred_boxes[:, 0] + pred_boxes[:, 2]) / 2
    pred_cy = (pred_boxes[:, 1] + pred_boxes[:, 3]) / 2
    gt_cx = (target_boxes[:, 0] + target_boxes[:, 2]) / 2
    gt_cy = (target_boxes[:, 1] + target_boxes[:, 3]) / 2
    d2 = (pred_cx - gt_cx) ** 2 + (pred_cy - gt_cy) ** 2

    # 宽高比一致性
    pred_w = pred_boxes[:, 2] - pred_boxes[:, 0]
    pred_h = pred_boxes[:, 3] - pred_boxes[:, 1]
    gt_w = target_boxes[:, 2] - target_boxes[:, 0]
    gt_h = target_boxes[:, 3] - target_boxes[:, 1]

    v = (4 / (torch.pi ** 2)) * (torch.atan(gt_w / (gt_h + 1e-7)) -
                                   torch.atan(pred_w / (pred_h + 1e-7))) ** 2
    with torch.no_grad():
        alpha = v / (1 - iou + v + 1e-7)

    ciou = iou - d2 / c2 - alpha * v
    loss = 1 - ciou
    return loss.mean()

7.6.4 NMS 变体

标准 NMS

Python
def nms(boxes, scores, iou_threshold=0.5):
    """非极大值抑制

    Args:
        boxes: (N, 4) 边界框 [x1, y1, x2, y2]
        scores: (N,) 置信度分数
        iou_threshold: IoU 阈值
    Returns:
        keep: 保留的框索引
    """
    order = scores.argsort()[::-1]    # 按分数降序排列
    keep = []

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

        if len(order) == 1:
            break

        # 计算当前最高分框与其余框的 IoU
        ious = np.array([  # np.array创建NumPy数组
            calculate_iou(boxes[i], boxes[j]) for j in order[1:]
        ])

        # 保留 IoU 小于阈值的框
        mask = ious < iou_threshold
        order = order[1:][mask]

    return keep

Soft-NMS

不直接删除高 IoU 的框,而是降低其置信度

\[s_i = \begin{cases} s_i \cdot e^{-\frac{\text{IoU}^2}{\sigma}} & \text{Gaussian} \\ s_i \cdot (1 - \text{IoU}) & \text{Linear} \end{cases}\]
Python
def soft_nms(boxes, scores, sigma=0.5, score_threshold=0.001):
    """Soft-NMS 实现"""
    N = len(boxes)
    indices = np.arange(N)

    for i in range(N):
        # 找当前最高分
        max_idx = np.argmax(scores[i:]) + i
        # 交换到当前位置
        boxes[[i, max_idx]] = boxes[[max_idx, i]]
        scores[[i, max_idx]] = scores[[max_idx, i]]
        indices[[i, max_idx]] = indices[[max_idx, i]]

        # 衰减重叠框的分数
        for j in range(i + 1, N):
            iou = calculate_iou(boxes[i], boxes[j])
            scores[j] *= np.exp(-(iou ** 2) / sigma)  # Gaussian 衰减

    keep = np.where(scores > score_threshold)[0]
    return indices[keep]

DIoU-NMS

DIoU 替代 IoU 作为重叠判断标准,同时考虑框的距离:

\[R_{DIoU} = \text{IoU} - \frac{d^2}{c^2}\]

Weighted NMS / Cluster NMS

  • Weighted NMS:对重叠框做加权平均(而非直接丢弃)
  • Cluster NMS:并行化 NMS,利用矩阵运算加速

📝 面试考点:Focal Loss 的 \(\gamma\) 参数作用(聚焦难分样本);CIoU Loss 对比 IoU/GIoU/DIoU 的改进点;NMS 手撕代码;Soft-NMS 的动机(遮挡场景不误删);ATSS 与 SimOTA 的自适应选样思路。


7.7 小目标检测

7.7.1 小目标的挑战

在 COCO 定义中,面积 < \(32^2\) 像素的为小目标。小目标检测困难在于:

挑战 原因
特征表示弱 经过多次下采样后特征消失
标注噪声大 1-2 像素标注偏差影响显著
正样本少 小目标在特征图上映射区域极小
上下文信息缺乏 感受野与目标尺度不匹配

7.7.2 多尺度特征融合

FPN 及其变体是解决多尺度问题的基础:

方法 融合方式 特点
FPN 自顶向下 + 横向连接 经典基线
PANet FPN + 自底向上路径 双向融合
BiFPN(EfficientDet) 加权双向融合 特征重要性加权
NAS-FPN NAS 搜索融合拓扑 自动架构设计

P2 高分辨率特征:使用 ¼ 下采样的 P2 特征层检测小目标,计算量增大但小目标 AP 显著提升。

7.7.3 SAHI 切片推理(Slicing Aided Hyper Inference)

核心思想:将大幅图像切成重叠小块(slices),分别推理后合并结果。

Python
# SAHI 切片推理(pip install sahi)
from sahi import AutoDetectionModel
from sahi.predict import get_sliced_prediction

# 加载检测模型
detection_model = AutoDetectionModel.from_pretrained(
    model_type='yolov8',
    model_path='yolov8s.pt',
    confidence_threshold=0.3,
    device='cuda:0',
)

# 切片推理
result = get_sliced_prediction(
    image='high_res_image.jpg',
    detection_model=detection_model,
    slice_height=640,           # 切片高度
    slice_width=640,            # 切片宽度
    overlap_height_ratio=0.2,   # 高度方向重叠比例
    overlap_width_ratio=0.2,    # 宽度方向重叠比例
    perform_standard_pred=True, # 同时做全图推理
    postprocess_type='NMM',     # 非极大合并
    postprocess_match_threshold=0.5,
)

# 可视化
result.export_visuals(export_dir='results/')
print(f"检测到 {len(result.object_prediction_list)} 个目标")

7.7.4 其他小目标策略

策略 方法
超分辨率 先对小目标区域超分再检测
特征增强 Dilated Conv、Deformable Conv 扩大感受野
Copy-Paste 复制小目标粘贴到图像中增加样本
密集 anchor 增加小尺度 anchor 密度
多尺度测试 TTA 输入多个尺度取并集

📝 面试考点:小目标检测难点分析;FPN/PANet/BiFPN 融合区别;SAHI 切片推理原理与适用场景;多尺度测试 TTA 的优缺点。


7.8 3D 目标检测简介

7.8.1 3D 检测任务定义

输出:3D 边界框 \((x, y, z, w, h, l, \theta)\) — 中心坐标、尺寸、朝向角。

输入模态

输入 代表方法 特点
点云(LiDAR) PointPillars, CenterPoint 精确 3D 信息
图像(单目/双目) FCOS3D, DETR3D 成本低、深度估计难
多模态融合 BEVFusion, TransFusion 互补优势

7.8.2 点云检测方法

PointPillars(2019)

将点云划分为柱状(pillars)网格 → 编码为 2D 伪图像 → 2D 检测网络。

Text Only
点云 → Pillar 划分 → PointNet 编码 → 2D 伪图像特征 → SSD 检测头

CenterPoint(2021)

基于 CenterNet 的 3D 检测框架:

Text Only
点云 → 3D Backbone (VoxelNet/PointPillars)
     → BEV 特征图
     → 中心点热力图 + 3D 属性回归 (z, size, yaw, velocity)
     → 可选: 第二阶段点特征精修

7.8.3 BEV(Bird's Eye View)感知

BEV 将多传感器信息统一到鸟瞰视角,是自动驾驶感知的主流范式:

Text Only
多视角相机图像 ──►  2D 特征提取
               视角变换 (LSS / Transformer)
                  BEV 特征图 ◄── LiDAR BEV 特征(可选融合)
                3D 检测 / 分割 / 预测

代表工作

方法 年份 视角变换方式 特点
LSS 2020 深度估计 + 投影 开创性
BEVDet 2022 LSS 改进 高效
BEVFormer 2022 时空注意力 Transformer 查询
BEVFusion 2022 点云 + 图像融合 多模态 SOTA

7.8.4 自动驾驶检测基准

数据集 传感器 类别 特点
KITTI LiDAR + 相机 8 经典基准
nuScenes 6 相机 + LiDAR + RADAR 10 360° 全景
Waymo Open 5 LiDAR + 5 相机 4 大规模高质量
Argoverse 2 2 LiDAR + 7 相机 30 丰富标注

📝 面试考点:3D 检测的输入模态对比;PointPillars 的 pillar 编码思路;BEV 感知的核心优势(统一坐标系、易于融合规划);Camera-only 3D 检测的深度估计难题。


7.9 实战项目:YOLOv8 自定义数据集训练与部署

7.9.1 项目概述

目标:使用 YOLOv8 在自定义数据集上训练安全帽检测模型,并导出部署。

Text Only
数据准备 → 标注 → 配置 → 训练 → 评估 → 导出 → ONNX推理部署

7.9.2 环境安装

Bash
pip install ultralytics
pip install labelimg          # 标注工具(可选)
pip install onnxruntime-gpu   # ONNX 部署

7.9.3 数据集结构(YOLO 格式)

Text Only
datasets/
├── helmet/
│   ├── images/
│   │   ├── train/
│   │   │   ├── 001.jpg
│   │   │   └── ...
│   │   └── val/
│   │       ├── 100.jpg
│   │       └── ...
│   ├── labels/
│   │   ├── train/
│   │   │   ├── 001.txt      # 每行: class cx cy w h (归一化)
│   │   │   └── ...
│   │   └── val/
│   │       ├── 100.txt
│   │       └── ...
│   └── helmet.yaml           # 数据集配置文件

7.9.4 数据集配置文件

YAML
# helmet.yaml
path: ./datasets/helmet       # 数据集根目录
train: images/train            # 训练集路径(相对 path)
val: images/val                # 验证集路径

# 类别定义
names:
  0: helmet                    # 佩戴安全帽
  1: no_helmet                 # 未佩戴安全帽
  2: person                    # 人

7.9.5 训练代码

Python
"""
YOLOv8 安全帽检测 — 完整训练脚本
"""
from ultralytics import YOLO

def train():
    """训练模型"""
    # 1. 加载预训练模型(迁移学习)
    model = YOLO('yolov8s.pt')    # small 版,平衡精度与速度

    # 2. 开始训练
    results = model.train(
        data='helmet.yaml',       # 数据集配置
        epochs=100,               # 训练轮数
        imgsz=640,                # 输入尺寸
        batch=16,                 # batch size
        device='0',               # GPU 设备号
        workers=8,                # 数据加载线程数

        # 优化器与学习率
        optimizer='SGD',          # 优化器
        lr0=0.01,                 # 初始学习率
        lrf=0.01,                 # 最终学习率 = lr0 * lrf
        momentum=0.937,           # SGD 动量
        weight_decay=0.0005,      # 权重衰减
        warmup_epochs=3,          # warmup 轮数

        # 数据增强
        mosaic=1.0,               # Mosaic 概率(最后 10 个 epoch 关闭)
        mixup=0.1,                # MixUp 概率
        copy_paste=0.1,           # CopyPaste 概率
        hsv_h=0.015,              # 色调增强
        hsv_s=0.7,                # 饱和度增强
        hsv_v=0.4,                # 明度增强
        degrees=0.0,              # 旋转角度
        translate=0.1,            # 平移比例
        scale=0.5,                # 缩放比例
        fliplr=0.5,               # 水平翻转概率

        # 保存与日志
        project='runs/helmet',    # 项目目录
        name='exp',               # 实验名称
        save=True,                # 保存 checkpoint
        save_period=10,           # 每 10 个 epoch 保存
        val=True,                 # 训练时验证
        plots=True,               # 生成训练曲线
    )
    return results

def validate():
    """验证模型"""
    model = YOLO('runs/helmet/exp/weights/best.pt')
    metrics = model.val(
        data='helmet.yaml',
        imgsz=640,
        batch=16,
        conf=0.25,
        iou=0.6,
        device='0',
    )

    print(f"mAP50    : {metrics.box.map50:.4f}")
    print(f"mAP50-95 : {metrics.box.map:.4f}")
    # 各类别 AP
    for i, name in enumerate(metrics.names.values()):
        print(f"  {name:15s}: AP50={metrics.box.ap50[i]:.4f}")

    return metrics

if __name__ == '__main__':
    train()
    validate()

7.9.6 推理与可视化

Python
from ultralytics import YOLO
from PIL import Image
import cv2

def inference_single(image_path):
    """单张图片推理"""
    model = YOLO('runs/helmet/exp/weights/best.pt')

    results = model(image_path, conf=0.3, iou=0.5)

    # 解析结果
    for r in results:
        boxes = r.boxes
        for box in boxes:
            cls_id = int(box.cls[0])
            conf = float(box.conf[0])
            xyxy = box.xyxy[0].cpu().numpy().astype(int)
            label = r.names[cls_id]
            print(f"[{label}] conf={conf:.2f} box={xyxy}")

    # 保存可视化
    annotated = results[0].plot()          # numpy (BGR)
    cv2.imwrite('result.jpg', annotated)
    print("结果已保存至 result.jpg")

def inference_video(video_path, output_path='output.mp4'):
    """视频推理"""
    model = YOLO('runs/helmet/exp/weights/best.pt')

    cap = cv2.VideoCapture(video_path)
    fps = int(cap.get(cv2.CAP_PROP_FPS))
    w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

    writer = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))

    frame_count = 0
    while cap.isOpened():
        ret, frame = cap.read()
        if not ret:
            break

        results = model(frame, conf=0.3, verbose=False)
        annotated = results[0].plot()
        writer.write(annotated)

        frame_count += 1
        if frame_count % 100 == 0:
            print(f"已处理 {frame_count} 帧")

    cap.release()
    writer.release()
    print(f"视频结果已保存至 {output_path}")

7.9.7 模型导出

Python
from ultralytics import YOLO

model = YOLO('runs/helmet/exp/weights/best.pt')

# 导出 ONNX(通用部署格式)
model.export(
    format='onnx',
    imgsz=640,
    simplify=True,         # 简化 ONNX 图
    opset=12,              # ONNX opset 版本
    dynamic=False,         # 固定输入尺寸(部署更快)
)

# 导出 TensorRT(NVIDIA GPU 高性能推理)
model.export(
    format='engine',
    imgsz=640,
    half=True,             # FP16 量化
    device='0',
)

# 导出 NCNN(移动端)
model.export(format='ncnn', imgsz=640)

# 导出 CoreML(iOS)
model.export(format='coreml', imgsz=640)

7.9.8 ONNX Runtime 部署推理

Python
import onnxruntime as ort
import numpy as np
import cv2

class YOLOv8ONNXDetector:
    """YOLOv8 ONNX 部署推理器"""

    def __init__(self, model_path, conf_thres=0.3, iou_thres=0.5):
        # 创建推理会话
        providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
        self.session = ort.InferenceSession(model_path, providers=providers)

        self.input_name = self.session.get_inputs()[0].name
        self.input_shape = self.session.get_inputs()[0].shape   # [1, 3, 640, 640]
        self.imgsz = self.input_shape[2]

        self.conf_thres = conf_thres
        self.iou_thres = iou_thres

    def preprocess(self, image):
        """预处理:letterbox resize + 归一化"""
        h, w = image.shape[:2]
        scale = min(self.imgsz / h, self.imgsz / w)
        new_h, new_w = int(h * scale), int(w * scale)

        # Resize
        resized = cv2.resize(image, (new_w, new_h))

        # Letterbox padding
        canvas = np.full((self.imgsz, self.imgsz, 3), 114, dtype=np.uint8)
        dh, dw = (self.imgsz - new_h) // 2, (self.imgsz - new_w) // 2
        canvas[dh:dh + new_h, dw:dw + new_w] = resized

        # BGR → RGB, HWC → CHW, 归一化
        blob = canvas[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0
        blob = np.expand_dims(blob, axis=0)    # (1, 3, 640, 640)

        return blob, scale, dw, dh

    def postprocess(self, output, scale, dw, dh):
        """后处理:解码 + NMS"""
        # YOLOv8 输出: (1, 4+num_classes, 8400) → 转置为 (8400, 4+C)
        preds = output[0].squeeze(0).T   # (8400, 4+C)  # squeeze压缩维度

        # 提取类别分数
        boxes = preds[:, :4]             # (8400, 4) cxcywh
        scores = preds[:, 4:]            # (8400, C)

        # 获取每个框的最大类别分数
        max_scores = scores.max(axis=1)
        class_ids = scores.argmax(axis=1)

        # 置信度过滤
        mask = max_scores > self.conf_thres
        boxes = boxes[mask]
        max_scores = max_scores[mask]
        class_ids = class_ids[mask]

        if len(boxes) == 0:
            return [], [], []

        # cxcywh → xyxy
        x1 = boxes[:, 0] - boxes[:, 2] / 2
        y1 = boxes[:, 1] - boxes[:, 3] / 2
        x2 = boxes[:, 0] + boxes[:, 2] / 2
        y2 = boxes[:, 1] + boxes[:, 3] / 2
        xyxy = np.stack([x1, y1, x2, y2], axis=1)

        # 还原到原图坐标
        xyxy[:, [0, 2]] = (xyxy[:, [0, 2]] - dw) / scale
        xyxy[:, [1, 3]] = (xyxy[:, [1, 3]] - dh) / scale

        # NMS
        keep = cv2.dnn.NMSBoxes(
            xyxy.tolist(),
            max_scores.tolist(),
            self.conf_thres,
            self.iou_thres,
        )

        if len(keep) > 0:
            keep = keep.flatten()
            return xyxy[keep], max_scores[keep], class_ids[keep]
        return [], [], []

    def detect(self, image):
        """完整检测流程"""
        blob, scale, dw, dh = self.preprocess(image)
        output = self.session.run(None, {self.input_name: blob})
        boxes, scores, class_ids = self.postprocess(output, scale, dw, dh)
        return boxes, scores, class_ids

    def draw(self, image, boxes, scores, class_ids, class_names):
        """绘制检测结果"""
        colors = [(0, 255, 0), (0, 0, 255), (255, 0, 0)]
        for box, score, cid in zip(boxes, scores, class_ids):
            x1, y1, x2, y2 = box.astype(int)
            color = colors[int(cid) % len(colors)]
            label = f"{class_names[int(cid)]} {score:.2f}"
            cv2.rectangle(image, (x1, y1), (x2, y2), color, 2)
            cv2.putText(image, label, (x1, y1 - 8),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
        return image

# ============ 使用示例 ============
if __name__ == '__main__':
    detector = YOLOv8ONNXDetector('best.onnx', conf_thres=0.3, iou_thres=0.5)

    image = cv2.imread('test.jpg')
    boxes, scores, class_ids = detector.detect(image)

    class_names = ['helmet', 'no_helmet', 'person']
    result = detector.draw(image, boxes, scores, class_ids, class_names)
    cv2.imwrite('onnx_result.jpg', result)
    print(f"ONNX 推理完成,检测到 {len(boxes)} 个目标")

7.9.9 性能优化技巧

优化方向 方法 效果
模型压缩 FP16 量化 推理速度 ×2,精度几乎无损
模型压缩 INT8 量化 速度进一步提升,需校准数据
推理框架 TensorRT NVIDIA GPU 最优
推理框架 OpenVINO Intel CPU/GPU 最优
推理框架 NCNN 移动端 ARM 最优
输入优化 固定 batch、固定尺寸 减少动态开销
后处理 Batched NMS / CUDA NMS 减少 CPU 瓶颈

📝 面试考点:YOLOv8 训练 pipeline;ONNX 导出与推理部署流程;Letterbox resize 的目的(保持宽高比+对齐);NMS 后处理在部署中的优化策略;FP16/INT8 量化对精度的影响。


7.10 面试高频题

Q1: 目标检测中 IoU 的计算方式,以及 GIoU / DIoU / CIoU 的区别?

IoU = 交集面积 / 并集面积,范围 [0, 1]。

改进版本: - GIoU\(\text{GIoU} = \text{IoU} - \frac{|C \setminus (A \cup B)|}{|C|}\),其中 \(C\) 是最小外接矩形。解决 IoU=0 时无梯度问题。 - DIoU:在 IoU 基础上加入中心点距离惩罚 \(\frac{d^2}{c^2}\),加速收敛。 - CIoU:在 DIoU 基础上增加宽高比一致性惩罚 \(\alpha v\),最常用。

关键差异:IoU 只关注重叠面积 → GIoU 考虑非重叠区域 → DIoU 考虑中心距离 → CIoU 再加宽高比。


Q2: Faster R-CNN 的完整工作流程?

  1. Backbone(ResNet + FPN):提取多尺度特征金字塔 P2-P5
  2. RPN:在每个特征点生成 9 个 anchor,预测前景/背景分数 + 边界框偏移;NMS 后输出 ~2000 个候选框
  3. RoI Align:将候选框映射到特征图并池化为固定大小(7×7)
  4. Head:两个全连接分支 — 分类(N+1 类 softmax)+ 回归(4×N 个偏移量)
  5. 训练:RPN loss(cls + reg)+ Head loss(cls + reg),联合端到端训练
  6. 推理:NMS 去重,输出最终检测框

Q3: YOLO v1 的设计思想及局限性?

设计:将图像划分为 S×S 网格,每个网格预测 B 个框 + C 个类别概率,单次前向传播完成检测(回归思路取代滑窗/候选区域)。

局限: - 每个网格只预测 2 个框 → 密集目标和小目标效果差 - 下采样过多丢失细节特征 - 泛化能力不足(异常宽高比目标) - 定位精度不如 Faster R-CNN


Q4: YOLOv5 与 YOLOv8 的关键区别?

维度 YOLOv5 YOLOv8
Anchor Anchor-Based (自动聚类) Anchor-Free
Head 耦合头 (cls+reg 共享) 解耦头 (分离)
标签分配 IoU 匹配 TAL (Task-Aligned)
Loss CIoU + BCE DFL + CIoU
Backbone C3 模块 C2f 模块
任务支持 检测 检测/分割/分类/姿态/OBB

Q5: NMS 算法步骤?Soft-NMS 的改进动机?

NMS 步骤: 1. 所有框按置信度降序排列 2. 取最高分框加入结果集 3. 计算其与剩余框的 IoU,移除 IoU > 阈值的框 4. 重复 2-3 直到无剩余框

Soft-NMS 动机:标准 NMS 在目标重叠场景(如行人密集)会误删正确框。Soft-NMS 不直接删除,而是用高斯或线性函数衰减重叠框置信度,保留被遮挡目标的检测。


Q6: Focal Loss 解决什么问题?\(\gamma\) 参数的作用?

解决 one-stage 检测器中正负样本严重不平衡 的问题(负样本:正样本≈1000:1)。

\(\text{FL}(p_t) = -\alpha_t (1-p_t)^\gamma \log(p_t)\)

  • \((1-p_t)^\gamma\)调制因子:当 \(p_t\) 大(易分类样本)时权重趋近 0 → 自动降低易分类样本贡献
  • \(\gamma\) 越大,对易分样本的抑制力度越强
  • 默认 \(\gamma=2\):使 \(p_t=0.9\) 的样本 loss 权重降为标准 CE 的 1%

Q7: DETR 的核心设计与优缺点?

核心设计: 1. CNN Backbone + Transformer Encoder-Decoder 2. Object Queries(可学习的位置查询) 3. 匈牙利匹配(二分图最优匹配,避免 NMS) 4. 集合预测 Loss = 分类 + L1 + GIoU

优点:端到端、无需 anchor/NMS/手工后处理、全局推理(对大目标和遮挡场景有优势)

缺点:收敛极慢(500 epochs)、小目标差(全局 attention 在低分辨率特征上难捕获)、计算量大


Q8: Anchor-Based 与 Anchor-Free 的本质区别?

  • Anchor-Based:在特征图每个位置预设多个先验框(anchor),预测相对 anchor 的偏移量。代表:Faster R-CNN、YOLOv3-v5。
  • Anchor-Free:直接预测目标属性。两种思路:
  • 逐像素回归(FCOS):每个特征点预测到真实框四条边的距离
  • 关键点检测(CenterNet):预测目标中心点热力图 + 尺寸

关键差异:Anchor-Based 需要大量超参数(尺度、比例、IoU 阈值),Anchor-Free 更简洁通用,是当前主流趋势。


Q9: FPN 如何解决多尺度检测问题?

FPN 构建自顶向下的特征金字塔: 1. 自底向上:Backbone 正常前向传播,得到 C2-C5 多层特征 2. 横向连接:1×1 卷积统一通道数至 256 3. 自顶向下:高层特征上采样 2× 后与低层横向连接相加 4. 输出卷积:3×3 卷积消除上采样混叠效应

效果:每级特征都同时拥有强语义信息(来自高层)和高分辨率细节(来自低层),P2 检测小目标,P5 检测大目标。


Q10: 小目标检测有哪些策略?

  1. 多尺度融合:FPN/PANet/BiFPN,使用高分辨率特征层 P2
  2. 切片推理:SAHI — 将大图切成重叠小块分别推理后合并
  3. 数据增强:Copy-Paste 复制小目标、Mosaic 拼接增加目标密度
  4. 增大输入分辨率:从 640 增大到 1280,计算量增大但小目标 AP 提升
  5. 多尺度测试 TTA:推理时用多个尺度取结果并集
  6. 特征增强:Deformable Conv、Dilated Conv 扩大感受野
  7. 密集 anchor 策略:增加小尺度 anchor 密度

Q11: 3D 目标检测与 2D 的核心区别?BEV 感知的优势?

核心区别: - 输出从 2D 框 \((x, y, w, h)\) 扩展为 3D 框 \((x, y, z, w, h, l, \theta)\) - 需要深度信息(来自 LiDAR 点云或单目深度估计) - 坐标系从图像像素坐标变为 3D 世界坐标

BEV 优势: - 统一坐标系:将多传感器信息投影到同一鸟瞰平面 - 无透视畸变:同一尺寸目标在 BEV 中大小一致 - 易于下游规划:自动驾驶的规划模块直接在 BEV 空间工作 - 多帧时序融合:在 BEV 空间做时序对齐更自然


Q12: 模型部署时如何优化目标检测推理速度?

层面 优化方法
模型层面 FP16/INT8 量化、知识蒸馏、剪枝、NAS 搜索轻量结构
框架层面 TensorRT (GPU) / OpenVINO (Intel) / NCNN (ARM) / CoreML (iOS)
输入层面 固定 batch size、固定输入尺寸、降低分辨率
后处理层面 Batched NMS、CUDA NMS、或使用 NMS-Free 模型(YOLOv10/DETR)
系统层面 预处理-推理-后处理 pipeline 并行、多 stream 并发推理
工程层面 预热(warmup)、内存池复用、零拷贝数据传输

量化精度影响(以 YOLOv8s 为例): - FP32 → FP16:mAP 下降 < 0.1%,速度 ×1.5-2 - FP16 → INT8(校准后):mAP 下降 ~0.5-1%,速度 ×2-3

📝 面试考点:以上 12 题均为大厂高频考题,重点掌握 IoU/NMS 手撕代码、Faster R-CNN 流程、YOLO 系列对比、Focal Loss 原理、FPN 结构、部署优化策略。


本章小结

核心知识图谱

Text Only
目标检测
├── 基础
│   ├── IoU / mAP / AP50 / AP75
│   ├── COCO 数据集与评价体系
│   └── 检测范式演进
├── 两阶段检测器
│   ├── R-CNN → Fast R-CNN → Faster R-CNN
│   ├── RPN / RoI Align / FPN
│   └── Cascade R-CNN (多级级联)
├── YOLO 系列
│   ├── v1(回归) → v3(多尺度) → v4(BoF/BoS)
│   ├── v5(工程化) → v8(Anchor-Free)
│   └── v10(NMS-Free) → v11(高效注意力)
├── Anchor-Free
│   ├── FCOS (逐像素回归 + centerness)
│   ├── CenterNet (中心点热力图)
│   └── CornerNet (角点检测)
├── DETR 系列
│   ├── DETR (Transformer + Hungarian)
│   ├── Deformable DETR (可变形注意力)
│   ├── DINO (去噪训练)
│   └── RT-DETR (实时 Transformer)
├── 关键技术
│   ├── 数据增强: Mosaic / MixUp / CopyPaste
│   ├── 标签分配: ATSS / SimOTA / TAL
│   ├── Loss: Focal Loss / CIoU Loss / DFL
│   └── NMS: 标准 / Soft / DIoU / Weighted
├── 小目标 & 3D 检测
│   ├── 多尺度融合 / SAHI 切片推理
│   └── 点云检测 / BEV 感知
└── 实战部署
    ├── YOLOv8 自定义训练
    ├── ONNX Runtime 推理
    └── 量化与推理优化

下一步

下一章08-图像分割.md — 学习语义分割、实例分割与全景分割


恭喜完成第 7 章! 🎉