第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)是衡量预测框与真实框重合度的核心指标:
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) 上取平均,结果通常更低且更严格。
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 检测范式演进¶
┌──────────────────────────────────────────────────────────────────────┐
│ 目标检测技术发展脉络 │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ 传统方法 ──► 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 | 超大规模 |
# 使用 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),再对每个区域分类与回归。
输入图像 → 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:分类 + 回归联合训练
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,实现端到端训练。
RPN 设计: - 在特征图每个位置放置 k 个 anchor(不同尺度 × 不同比例) - 典型设置:3 个尺度 × 3 个比例 = 9 个 anchor / 位置 - 输出:前景/背景分数 + 边界框偏移
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 使用双线性插值消除量化误差:
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 构建自顶向下的多尺度特征金字塔,让每个尺度都拥有丰富语义信息:
┌────────┐
C5 (1/32) ──────►│ P5 │
↑ lateral │ 256ch │
C4 (1/16) ──►+──►│ P4 │ ← 自顶向下路径(上采样 + 横向连接)
↑ lateral │ 256ch │
C3 (1/8) ──►+──►│ P3 │
↑ lateral │ 256ch │
C2 (1/4) ──►+──►│ P2 │
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 阈值:
- 每一级使用更严格的正样本阈值,逐步逼近精确定位
- 解决了单一 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\)
局限:每个网格只预测 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 Freebies 和 Bag 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 |
# 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 框的四边距离。
特征点 (x, y) → 预测 (l, t, r, b) + class + centerness
l = x - x0 (到左边距离)
t = y - y0 (到上边距离)
r = x1 - x (到右边距离)
b = y1 - y (到下边距离)
Center-ness:抑制远离目标中心的低质量框
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)
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。
输入图像 → CNN Backbone → Transformer Encoder → Transformer Decoder → FFN → 预测
↑
Object Queries (可学习)
关键组件:
- Object Queries:\(N\) 个可学习的查询向量(通常 \(N=100\)),每个负责产生一个预测
- 二分图匹配(Hungarian Matching):将预测与 GT 一一最优匹配
- 集合预测 Loss:基于匹配结果计算分类 + L1 + GIoU loss
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) - 多尺度特征融合
- 收敛速度提升 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)
# 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:
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\)
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)¶
核心:根据统计信息自适应选择正样本阈值。
- 对每个 GT,在每个 FPN 层选择 top-k(如 k=9)最近 anchor
- 计算这些 anchor 与 GT 的 IoU
- 以 IoU 的均值 + 标准差作为正样本阈值
- IoU 大于阈值且中心在 GT 框内的 anchor 为正样本
SimOTA(YOLOX, 2021)¶
核心:简化的 OTA(Optimal Transport Assignment)。
- 计算每个预测与 GT 的代价矩阵(cls cost + reg cost)
- 对每个 GT 动态确定正样本数量 \(k\)(根据 IoU 前 k 个之和取整)
- 选择代价最低的 \(k\) 个预测作为正样本
TAL(Task-Aligned Assignment, YOLOv8)¶
同时考虑分类和定位的对齐程度:
其中 \(s\) 是分类分数,\(u\) 是 IoU,\(\alpha = 0.5, \beta = 6.0\)。选择 alignment metric 最大的 top-k 作为正样本。
7.6.3 Loss 函数¶
Focal Loss(2017, RetinaNet)¶
解决 正负样本极度不平衡 问题:
- \((1 - p_t)^\gamma\) 降低已分类样本的权重 → 聚焦难分样本
- 默认 \(\gamma = 2, \alpha = 0.25\)
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\) | 最常用 |
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¶
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 的框,而是降低其置信度:
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 作为重叠判断标准,同时考虑框的距离:
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),分别推理后合并结果。
# 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 检测网络。
CenterPoint(2021)¶
基于 CenterNet 的 3D 检测框架:
点云 → 3D Backbone (VoxelNet/PointPillars)
→ BEV 特征图
→ 中心点热力图 + 3D 属性回归 (z, size, yaw, velocity)
→ 可选: 第二阶段点特征精修
7.8.3 BEV(Bird's Eye View)感知¶
BEV 将多传感器信息统一到鸟瞰视角,是自动驾驶感知的主流范式:
多视角相机图像 ──► 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 在自定义数据集上训练安全帽检测模型,并导出部署。
7.9.2 环境安装¶
7.9.3 数据集结构(YOLO 格式)¶
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 数据集配置文件¶
# helmet.yaml
path: ./datasets/helmet # 数据集根目录
train: images/train # 训练集路径(相对 path)
val: images/val # 验证集路径
# 类别定义
names:
0: helmet # 佩戴安全帽
1: no_helmet # 未佩戴安全帽
2: person # 人
7.9.5 训练代码¶
"""
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 推理与可视化¶
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 模型导出¶
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 部署推理¶
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 的完整工作流程?¶
答:
- Backbone(ResNet + FPN):提取多尺度特征金字塔 P2-P5
- RPN:在每个特征点生成 9 个 anchor,预测前景/背景分数 + 边界框偏移;NMS 后输出 ~2000 个候选框
- RoI Align:将候选框映射到特征图并池化为固定大小(7×7)
- Head:两个全连接分支 — 分类(N+1 类 softmax)+ 回归(4×N 个偏移量)
- 训练:RPN loss(cls + reg)+ Head loss(cls + reg),联合端到端训练
- 推理: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: 小目标检测有哪些策略?¶
答:
- 多尺度融合:FPN/PANet/BiFPN,使用高分辨率特征层 P2
- 切片推理:SAHI — 将大图切成重叠小块分别推理后合并
- 数据增强:Copy-Paste 复制小目标、Mosaic 拼接增加目标密度
- 增大输入分辨率:从 640 增大到 1280,计算量增大但小目标 AP 提升
- 多尺度测试 TTA:推理时用多个尺度取结果并集
- 特征增强:Deformable Conv、Dilated Conv 扩大感受野
- 密集 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 结构、部署优化策略。
本章小结¶
核心知识图谱¶
目标检测
├── 基础
│ ├── 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 章! 🎉