这是「Python 计算机视觉三部曲」的最终篇。 前两篇解决了“像素处理”和“图像分类”,这一篇解决“多目标识别 + 精确定位 + 工程部署”。

你会在这篇里打通目标检测完整链路:

  • 理解检测任务与评估体系(IoU / NMS / mAP)
  • 掌握 YOLO 系列关键演进逻辑(从原理到选型)
  • 完成从训练到导出再到上线部署的闭环

阅读导航

  • 想先懂核心概念:看 第 1~4 章
  • 想尽快训练出可用模型:看 第 5~9 章
  • 想落地线上服务:看 第 10~11 章

系列导航

  • 第一篇:计算机视觉与图像处理入门 — 从像素到特征
  • 第二篇:深度学习与计算机视觉 — 从 CNN 到实战
  • 第三篇:目标检测实战 — 从 YOLO 到部署(本文)

1. 什么是目标检测?

1.1 从分类到检测

图像分类回答一个问题:这张图里主要有什么?

目标检测回答两个问题:

  1. 图里有哪些物体?(分类)
  2. 每个物体在哪个位置?(定位)

输出格式:每个检测到的物体用一个边界框(Bounding Box)+ 类别标签 + 置信度表示。

1
2
3
4
5
输出: [
{class: "cat", confidence: 0.95, bbox: [x1, y1, x2, y2]},
{class: "dog", confidence: 0.87, bbox: [x1, y1, x2, y2]},
{class: "person", confidence: 0.92, bbox: [x1, y1, x2, y2]},
]

1.2 目标检测的应用场景

领域 应用
自动驾驶 行人、车辆、交通标志检测
安防监控 异常行为、入侵检测
工业质检 产品缺陷检测
医疗影像 病变区域检测
零售 货架商品识别、自助结算
农业 病虫害检测、果实计数
体育 运动员追踪、动作分析

1.3 目标检测 vs 图像分割

任务 输出 粒度
目标检测 矩形框 + 类别 物体级
语义分割 逐像素类别 像素级(不区分实例)
实例分割 逐像素实例 像素级(区分实例)
全景分割 语义 + 实例 最精细

2. 目标检测基础概念

2.1 边界框(Bounding Box)

边界框是目标检测的基本表示单位,有两种常见格式:

格式 表示 示例
xyxy 左上角 + 右下角坐标 [100, 50, 300, 400]
xywh 中心点 + 宽高 [200, 225, 200, 350]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 两种格式互转
def xyxy_to_xywh(box):
x1, y1, x2, y2 = box
cx = (x1 + x2) / 2
cy = (y1 + y2) / 2
w = x2 - x1
h = y2 - y1
return [cx, cy, w, h]

def xywh_to_xyxy(box):
cx, cy, w, h = box
x1 = cx - w / 2
y1 = cy - h / 2
x2 = cx + w / 2
y2 = cy + h / 2
return [x1, y1, x2, y2]

2.2 交并比(IoU, Intersection over Union)

IoU 衡量两个框的重叠程度,是目标检测最基础的指标。

1
2
3
4
5
IoU = 交集面积 / 并集面积

IoU = 1.0: 完全重合
IoU = 0.5: 一般认为是"检测正确"的阈值
IoU = 0.0: 完全不重叠
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import numpy as np

def calculate_iou(box1, box2):
"""计算两个框的 IoU(xyxy 格式)"""
# 计算交集
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

# 演示
box_a = [0, 0, 100, 100]
box_b = [50, 50, 150, 150]
print(f"IoU = {calculate_iou(box_a, box_b):.4f}") # 约 0.1429

2.3 锚框(Anchor Box)

锚框是预设的"参考框",模型在锚框基础上进行微调(偏移量预测),而非直接预测框的绝对坐标。

为什么需要锚框? 直接预测框坐标太难了——不同物体的尺度差异巨大。锚框提供了多个尺度和比例的"起点",模型只需学习从锚框到真实框的微调量。

1
预测框 = 锚框 + 微调偏移量

YOLOv8 之后的版本引入了 Anchor-Free 设计,不再需要预设锚框,而是直接预测中心点和宽高,简化了流程。

2.4 非极大值抑制(NMS, Non-Maximum Suppression)

模型往往会对同一个物体检测出多个重叠的框。NMS 用来去除冗余的检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def nms(boxes, scores, iou_threshold=0.5):
"""非极大值抑制"""
# 按置信度排序
order = scores.argsort()[::-1]
keep = []

while order.size > 0:
# 取当前最高分的框
i = order[0]
keep.append(i)

if order.size == 1:
break

# 计算剩余框与当前框的 IoU
remaining = order[1:]
ious = np.array([calculate_iou(boxes[i], boxes[j]) for j in remaining])

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

return keep

# 演示
boxes = np.array([
[10, 10, 100, 100], # 猫 - 框1
[15, 12, 105, 98], # 猫 - 框2(与框1重叠)
[200, 200, 300, 350], # 狗 - 框1
])
scores = np.array([0.95, 0.80, 0.90])

keep = nms(boxes, scores, iou_threshold=0.5)
print(f"NMS 后保留的框索引: {keep}") # [0, 2]

NMS 流程图解

1
2
3
4
5
6
7
1. 所有检测框按置信度排序: [0.95, 0.90, 0.80]
2. 取最高分框 A (0.95),加入结果
3. 计算 A 与其余框的 IoU,删除 IoU > 阈值的框(0.80 被删除)
4. 剩余框中取最高分框 B (0.90),加入结果
5. 计算 B 与其余框的 IoU,删除 IoU > 阈值的框(无)
6. 结束
结果: [A, B]

2.5 置信度(Confidence)与类别概率

每个检测框有两个分数:

  • 目标置信度(Objectness):这个位置是否有物体?
  • 类别概率(Class Probability):如果有物体,是什么类别?
1
最终置信度 = 目标置信度 × 类别概率

例如:目标置信度 = 0.9,类别"猫"的概率 = 0.95,则最终置信度 = 0.9 × 0.95 = 0.855。


3. 目标检测算法演进

3.1 Two-Stage vs One-Stage

目标检测算法分为两大阵营:

维度 Two-Stage One-Stage
代表 R-CNN 系列 YOLO 系列、SSD
流程 先找候选区→再分类 直接预测框+类别
精度 中高(不断接近 Two-Stage)
速度 慢(5-20 FPS) 快(30-150 FPS)
适用 精度优先场景 实时场景

Two-Stage 思路:先"看哪些地方可能有东西"(Region Proposal),再"确定每个地方是什么"(Classification + Regression)。

One-Stage 思路:一步到位——把图片分成网格,每个网格直接预测框和类别。

3.2 里程碑时间线

1
2
3
4
5
6
7
8
9
10
2014: R-CNN → SPPNet → Fast R-CNN
2015: Faster R-CNN (Two-Stage 巅峰) | YOLOv1 (One-Stage 开山) | SSD
2016: YOLOv2 (YOLO9000)
2017: YOLOv3 | FPN | RetinaNet (Focal Loss)
2018: Mask R-CNN (实例分割)
2020: YOLOv4 | YOLOv5 | DETR (Transformer 检测)
2021: YOLOX | YOLOv6 | YOLOv7
2022: DINO | Co-DETR
2023: YOLOv8 | RT-DETR
2024: YOLOv9 | YOLOv10 | YOLOv11

4. YOLO 系列深度解析

4.1 YOLOv1(2015)— “You Only Look Once”

核心思想:将检测问题转化为回归问题——一次前向传播同时预测所有框。

1
2
3
4
1. 将图片分成 S×S 网格(7×7)
2. 每个网格预测 B 个框(2个)+ 置信度
3. 每个网格预测 C 个类别概率(20类)
4. 输出: S × S × (B×5 + C) = 7×7×30

优点:极快(45 FPS),全局推理减少背景误检。
缺点:小物体检测差,每个网格只能预测 2 个框(重叠物体漏检)。

4.2 YOLOv2 / YOLO9000(2016)

改进:

  • Batch Normalization:加速收敛
  • Anchor Boxes:引入锚框,使用 K-means 聚类自动选择
  • 多尺度训练:每 10 批随机改变输入尺寸
  • Darknet-19:更高效的特征提取网络

4.3 YOLOv3(2018)— 经典之作

三大改进:

  1. 多尺度检测:在 3 个不同尺度上检测,大幅提升小物体检测
  2. Darknet-53:引入残差连接,更深的网络
  3. 独立分类器:用 Logistic 分类替代 Softmax(支持多标签)
1
2
3
4
特征金字塔:
13×13 → 检测大物体
26×26 → 检测中物体
52×52 → 检测小物体

4.4 YOLOv4(2020)

“Bag of Freebies” + “Bag of Specials”:

  • Mosaic 数据增强:4 张图拼接成 1 张
  • CIoU Loss:更好的框回归损失
  • CSPDarknet53:跨阶段部分连接
  • SPP / PANet:更好的特征融合

4.5 YOLOv5(2020)— 工程化标杆

YOLOv5 由 Ultralytics 开发,虽然不是 YOLO 原作者的作品,但它的工程质量易用性让它成为最广泛使用的 YOLO 版本。

1
2
# YOLOv5 的简洁使用方式
pip install ultralytics

特点:

  • 纯 PyTorch 实现(无 Darknet 依赖)
  • 多种模型尺寸:n/s/m/l/x
  • 完整的训练/推理/导出工具链
  • 自动超参数进化

4.6 YOLOv8(2023)— 现代检测标杆

YOLOv8 是目前最推荐的版本,核心创新:

特性 YOLOv5 YOLOv8
检测头 Anchor-based Anchor-free
任务 仅检测 检测 + 分割 + 分类 + 姿态
损失函数 CIoU + BCE DFL + CIoU + VFL
骨干网络 CSPDarknet CSPDarknet + C2f
Neck PANet C2f + PAN
激活函数 SiLU SiLU

Anchor-free 的好处

  • 不需要手动设计锚框
  • 减少超参数
  • 更好地适应不同数据集

C2f 模块(Cross Stage Partial Bottleneck with 2 convolutions):

1
2
3
输入 → 1×1 Conv → 分成两路
├─ 一路直接连到输出
└─ 另一路经过多个 Bottleneck → 连到输出

相比 YOLOv5 的 C3 模块,C2f 在保持计算量的同时获得更丰富的梯度流。

4.7 YOLO 模型尺寸对比

模型 参数量(M) FLOPs(G) COCO mAP50-95 速度(V100, ms)
YOLOv8n 3.2 8.7 37.3 1.2
YOLOv8s 11.2 28.6 44.9 2.1
YOLOv8m 25.9 78.9 50.2 4.7
YOLOv8l 43.7 165.2 52.9 7.1
YOLOv8x 68.2 257.8 53.9 10.8

💡 选型建议:移动端/嵌入式 → n/s;服务器/高精度 → m/l;极致精度 → x。大部分项目 s 或 m 就够了。


5. 环境搭建与 YOLOv8 入门

5.1 安装

1
2
3
4
5
6
7
8
# 安装 ultralytics(包含 YOLOv8)
pip install ultralytics

# 验证
yolo version

# 如果有 GPU,确保 PyTorch CUDA 可用
python -c "import torch; print(f'CUDA: {torch.cuda.is_available()}')"

5.2 快速推理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from ultralytics import YOLO

# 加载预训练模型(首次运行会自动下载)
model = YOLO('yolov8n.pt') # nano 版本

# 对图片进行推理
results = model('test_image.jpg')

# 显示结果
results[0].show()

# 获取检测信息
for result in results:
boxes = result.boxes
for box in boxes:
cls_id = int(box.cls[0])
conf = float(box.conf[0])
xyxy = box.xyxy[0].tolist()
cls_name = model.names[cls_id]
print(f"检测到: {cls_name}, 置信度: {conf:.2f}, 位置: {[round(x,1) for x in xyxy]}")

5.3 命令行推理

1
2
3
4
5
6
7
8
9
10
11
# 图片推理
yolo detect predict model=yolov8n.pt source='test_image.jpg'

# 视频推理
yolo detect predict model=yolov8n.pt source='video.mp4'

# 摄像头实时推理
yolo detect predict model=yolov8n.pt source=0

# 保存结果
yolo detect predict model=yolov8n.pt source='test_image.jpg' save=True

5.4 YOLOv8 支持的任务

任务 模型后缀 命令
检测 .pt yolo detect
分割 -seg.pt yolo segment
分类 -cls.pt yolo classify
姿态估计 -pose.pt yolo pose

6. 数据标注与数据集准备

6.1 数据集组织结构

YOLOv8 期望的数据集结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
dataset/
├── images/
│ ├── train/
│ │ ├── img001.jpg
│ │ ├── img002.jpg
│ │ └── ...
│ ├── val/
│ │ ├── img101.jpg
│ │ └── ...
│ └── test/ (可选)
│ └── ...
└── labels/
├── train/
│ ├── img001.txt (与图片同名)
│ ├── img002.txt
│ └── ...
├── val/
│ ├── img101.txt
│ └── ...
└── test/
└── ...

6.2 标签格式(YOLO 格式)

每行代表一个标注:类别ID 中心X 中心Y 宽 高(归一化到 [0,1])

1
2
3
# img001.txt
0 0.5125 0.5370 0.2350 0.4810
1 0.7680 0.3120 0.1540 0.2230
  • 类别ID:从 0 开始的整数
  • 中心X/中心Y:边界框中心点相对于图片宽高的比例
  • 宽/高:边界框宽高相对于图片宽高的比例
1
2
3
4
5
6
7
8
9
10
11
12
13
# 将 xyxy 像素坐标转为 YOLO 格式
def xyxy_to_yolo(box, img_width, img_height, class_id):
"""将 (x1,y1,x2,y2) 像素坐标转为 YOLO 格式"""
x1, y1, x2, y2 = box
cx = ((x1 + x2) / 2) / img_width
cy = ((y1 + y2) / 2) / img_height
w = (x2 - x1) / img_width
h = (y2 - y1) / img_height
return f"{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}"

# 示例
result = xyxy_to_yolo([100, 50, 300, 400], 640, 480, class_id=0)
print(result) # 0 0.312500 0.468750 0.312500 0.729167

6.3 标注工具推荐

工具 特点 适用场景
LabelImg 经典,离线,支持 YOLO/VOC 格式 小规模标注
Label Studio Web 界面,多任务,团队协作 中大规模标注
CVAT 功能最全,支持视频标注,自动标注辅助 专业标注团队
Roboflow 云端,内置数据增强,导出多格式 快速原型
Make Sense 浏览器端,无需安装 临时标注

6.4 使用 Label Studio 标注

1
2
3
4
5
6
7
8
9
10
# 安装
pip install label-studio

# 启动
label-studio start

# 浏览器打开 http://localhost:8080
# 1. 创建项目,选择 "Object Detection with Bounding Boxes"
# 2. 上传图片
# 3. 标注完成后导出为 YOLO 格式

6.5 数据集配置文件

创建 dataset.yaml

1
2
3
4
5
6
7
8
9
10
# dataset.yaml
path: ./dataset # 数据集根目录
train: images/train # 训练图片路径(相对于 path)
val: images/val # 验证图片路径

# 类别
names:
0: cat
1: dog
2: bird

6.6 数据质量检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import os
import cv2
from pathlib import Path

def check_dataset(dataset_path):
"""检查数据集的完整性和质量"""
img_dir = Path(dataset_path) / 'images' / 'train'
lbl_dir = Path(dataset_path) / 'labels' / 'train'

issues = []

for img_path in img_dir.glob('*.jpg'):
# 检查对应的标签文件是否存在
lbl_path = lbl_dir / f"{img_path.stem}.txt"
if not lbl_path.exists():
issues.append(f"缺失标签: {lbl_path}")
continue

# 检查图片是否可读
img = cv2.imread(str(img_path))
if img is None:
issues.append(f"无法读取图片: {img_path}")
continue

# 检查标签格式
with open(lbl_path) as f:
for line_num, line in enumerate(f, 1):
parts = line.strip().split()
if len(parts) != 5:
issues.append(f"{lbl_path}{line_num}行: 格式错误")
continue

try:
cls, cx, cy, w, h = map(float, parts)
if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 < w <= 1 and 0 < h <= 1):
issues.append(f"{lbl_path}{line_num}行: 坐标超出范围")
except ValueError:
issues.append(f"{lbl_path}{line_num}行: 数值解析错误")

if issues:
print(f"发现 {len(issues)} 个问题:")
for issue in issues[:20]: # 最多显示 20 个
print(f" - {issue}")
else:
print("数据集检查通过!")

return issues

# 运行检查
check_dataset('./dataset')

7. 实战:训练自定义目标检测器

7.1 场景:安全帽检测

我们以"工地安全帽检测"为例,训练一个能检测"戴安全帽的人"和"没戴安全帽的人"的模型。

7.2 数据准备

假设我们已经有标注好的数据集,组织如下:

1
2
3
4
5
6
7
8
helmet_dataset/
├── images/
│ ├── train/ (800 张)
│ └── val/ (200 张)
├── labels/
│ ├── train/
│ └── val/
└── dataset.yaml

dataset.yaml

1
2
3
4
5
6
7
path: ./helmet_dataset
train: images/train
val: images/val

names:
0: helmet # 戴安全帽
1: no_helmet # 未戴安全帽

7.3 训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from ultralytics import YOLO

# 加载预训练模型(迁移学习)
model = YOLO('yolov8s.pt') # small 版本,平衡精度和速度

# 开始训练
results = model.train(
data='helmet_dataset/dataset.yaml',
epochs=100,
imgsz=640,
batch=16,
patience=20, # 早停耐心值
lr0=0.01, # 初始学习率
lrf=0.01, # 最终学习率(相对于 lr0)
weight_decay=0.0005, # 权重衰减
warmup_epochs=3, # 预热 epoch 数
augment=True, # 数据增强
mosaic=1.0, # Mosaic 增强
mixup=0.1, # Mixup 增强
device=0, # GPU 编号,'cpu' 表示使用 CPU
workers=8, # 数据加载线程数
project='helmet_detection',
name='yolov8s_train',
exist_ok=True,
)

print(f"训练完成! 最佳 mAP50: {results['metrics/mAP50(B)']:.4f}")

7.4 命令行训练

1
2
3
4
5
6
7
8
9
yolo detect train \
model=yolov8s.pt \
data=helmet_dataset/dataset.yaml \
epochs=100 \
imgsz=640 \
batch=16 \
patience=20 \
project=helmet_detection \
name=yolov8s_train

7.5 训练过程可视化

Ultralytics 会自动保存训练日志和可视化,位于 runs/detect/train/

1
2
3
4
5
6
7
8
9
10
runs/detect/train/
├── weights/
│ ├── best.pt # 最佳模型
│ └── last.pt # 最后一个 epoch 的模型
├── confusion_matrix.png
├── results.png # 损失和指标曲线
├── PR_curve.png # Precision-Recall 曲线
├── F1_curve.png # F1 曲线
├── train_batch*.jpg # 训练样本可视化
└── val_batch*_pred.jpg # 验证预测可视化
1
2
3
# 用 TensorBoard 查看训练曲线
# pip install tensorboard
# tensorboard --logdir runs/detect/train

7.6 推理验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from ultralytics import YOLO

# 加载训练好的模型
model = YOLO('helmet_detection/yolov8s_train/weights/best.pt')

# 单张图片推理
results = model('test_image.jpg', conf=0.5, iou=0.45)

# 可视化
results[0].show()

# 保存带标注的图片
results[0].save('result.jpg')

# 视频推理
results = model('test_video.mp4', conf=0.5, save=True)

# 摄像头实时推理
results = model(source=0, conf=0.5, show=True)

7.7 批量推理与结果解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from ultralytics import YOLO
import json

model = YOLO('helmet_detection/yolov8s_train/weights/best.pt')

# 批量推理
results = model(
source='test_images/', # 文件夹路径
conf=0.5,
iou=0.45,
save=True,
save_txt=True, # 保存预测标签
save_conf=True, # 保存置信度
)

# 解析结果
detections = []
for result in results:
img_path = result.path
for box in result.boxes:
detection = {
'image': img_path,
'class': model.names[int(box.cls[0])],
'confidence': float(box.conf[0]),
'bbox': box.xyxy[0].tolist(), # [x1, y1, x2, y2]
}
detections.append(detection)

# 保存为 JSON
with open('detections.json', 'w', encoding='utf-8') as f:
json.dump(detections, f, ensure_ascii=False, indent=2)

print(f"共检测到 {len(detections)} 个目标")

8. 模型评估:mAP 详解

8.1 Precision、Recall 与 F1

指标 公式 含义
Precision(精确率) TP / (TP + FP) 预测为正的样本中,有多少是对的
Recall(召回率) TP / (TP + FN) 实际为正的样本中,有多少被找到
F1 2×P×R/(P+R) P 和 R 的调和平均
1
2
3
4
TP (True Positive):  预测为正,实际为正 ✓
FP (False Positive): 预测为正,实际为负 ✗
FN (False Negative): 预测为负,实际为正 ✗
TN (True Negative): 预测为负,实际为负 ✓

8.2 AP(Average Precision)

AP 是 Precision-Recall 曲线下的面积。

1
2
3
4
1. 对所有预测按置信度排序
2. 计算每个阈值下的 Precision 和 Recall
3. 绘制 P-R 曲线
4. AP = 曲线下面积

8.3 mAP(mean Average Precision)

mAP 是所有类别 AP 的平均值。

指标 含义 IoU 阈值
mAP@0.5 (mAP50) IoU ≥ 0.5 即算检测正确 0.5
mAP@0.5:0.95 (mAP50-95) IoU 从 0.5 到 0.95(步长 0.05)的平均值 0.5~0.95
1
2
3
4
5
6
7
8
9
# 使用 YOLOv8 内置验证
from ultralytics import YOLO

model = YOLO('helmet_detection/yolov8s_train/weights/best.pt')
metrics = model.val(data='helmet_dataset/dataset.yaml')

print(f"mAP50: {metrics.box.map50:.4f}") # 所有类别的 mAP@0.5
print(f"mAP50-95: {metrics.box.map:.4f}") # 所有类别的 mAP@0.5:0.95
print(f"各类别 mAP50: {metrics.box.maps}") # 每个类别的 mAP@0.5

8.4 mAP 通俗解释

mAP@0.5 = 0.85 意味着:如果你的框和真实框重叠度达到 50%,那么平均来看,模型在"找到且分对"这件事上得了 85 分。

mAP@0.5:0.95 = 0.55 意味着:在更严格的标准下(框要越来越准),平均得分 55 分。这个指标更能反映检测框的精确度。


9. 模型优化:超参数调优

9.1 YOLOv8 超参数优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from ultralytics import YOLO

model = YOLO('yolov8s.pt')

# 自动超参数搜索(使用遗传算法)
model.tune(
data='helmet_dataset/dataset.yaml',
epochs=30,
iterations=300, # 搜索 300 组超参数
optimizer='AdamW',
plots=True,
save=True,
val=True,
)

# 最佳超参数保存在 runs/detect/tune/best_hyperparameters.yaml

9.2 关键超参数说明

超参数 默认值 说明 调优建议
lr0 0.01 初始学习率 小数据集降低到 0.001
lrf 0.01 最终学习率比率 一般不动
momentum 0.937 SGD 动量 0.8~0.98
weight_decay 0.0005 权重衰减 0.0001~0.001
warmup_epochs 3 预热 小数据集可减少到 1
box 7.5 框损失权重 框不准时增大
cls 0.5 分类损失权重 分类不准时增大
dfl 1.5 DFL 损失权重 一般不动
mosaic 1.0 Mosaic 概率 后期可降低到 0.5
mixup 0.0 Mixup 概率 0.1~0.3
degrees 0.0 旋转角度 10~30
translate 0.1 平移比例 0~0.2

9.3 训练策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 策略一:渐进式训练
# Phase 1: 冻结骨干网络,只训练头部
model = YOLO('yolov8s.pt')
model.train(data='dataset.yaml', epochs=20, freeze=10) # 冻结前 10 层

# Phase 2: 解冻全部,小学习率微调
model = YOLO('runs/detect/train/weights/last.pt')
model.train(data='dataset.yaml', epochs=50, lr0=0.001)

# 策略二:多尺度训练
model.train(data='dataset.yaml', imgsz=640, epochs=100, scale=0.5)

# 策略三:降低 Mosaic(后期减少增强有利于收敛)
model.train(data='dataset.yaml', epochs=100, mosaic=0.5, close_mosaic=10) # 最后 10 epoch 关闭

9.4 提升检测效果的 Checklist

1
2
3
4
5
6
7
8
9
□ 数据量够吗?(每类至少 100~1500 张)
□ 数据质量好吗?(标注准确、无遗漏)
□ 数据增强合理吗?(不过度、不破坏语义)
□ 模型尺寸选对了吗?(小数据集用 s/m)
□ 学习率合适吗?(太大震荡,太小收敛慢)
□ 训练够久吗?(看 loss 曲线是否收敛)
□ 过拟合了吗?(train loss↓但 val loss↑)
□ 小物体检测差?(试试增大 imgsz 或添加小物体层)
□ 类别不平衡?(少数类过采样或加权)

10. 模型导出:ONNX 与 TensorRT

10.1 为什么要导出?

PyTorch 模型(.pt)适合训练和快速推理,但部署时需要更高效的格式。

格式 速度 兼容性 文件大小 适用场景
PyTorch (.pt) 基准 Python 生态 训练、快速原型
ONNX (.onnx) 1.2-2× 广泛 跨框架、通用部署
TensorRT (.engine) 3-10× NVIDIA GPU GPU 生产部署
OpenVINO 2-5× Intel 硬件 Intel CPU/GPU/VPU
CoreML (.mlmodel) 2-5× Apple 生态 iOS/macOS
TFLite (.tflite) 2-5× Android/Edge 移动端、嵌入式

10.2 导出 ONNX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from ultralytics import YOLO

model = YOLO('helmet_detection/yolov8s_train/weights/best.pt')

# 导出 ONNX
model.export(
format='onnx',
imgsz=640,
simplify=True, # 简化模型
dynamic=False, # 固定输入尺寸(部署推荐)
opset=12, # ONNX 算子版本
)

# 导出的文件: best.onnx

10.3 验证 ONNX 模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import onnx
import onnxruntime as ort
import numpy as np
import cv2

# 1. 检查模型有效性
onnx_model = onnx.load('best.onnx')
onnx.checker.check_model(onnx_model)
print("ONNX 模型检查通过!")

# 2. 用 ONNX Runtime 推理
session = ort.InferenceSession('best.onnx')

# 获取输入信息
input_name = session.get_inputs()[0].name
input_shape = session.get_inputs()[0].shape
print(f"输入名: {input_name}, 形状: {input_shape}") # [1, 3, 640, 640]

# 预处理
img = cv2.imread('test_image.jpg')
img_resized = cv2.resize(img, (640, 640))
img_rgb = cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB)
img_normalized = img_rgb.astype(np.float32) / 255.0
img_transposed = np.transpose(img_normalized, (2, 0, 1))
input_tensor = np.expand_dims(img_transposed, axis=0)

# 推理
outputs = session.run(None, {input_name: input_tensor})
print(f"输出形状: {[o.shape for o in outputs]}")

# 3. 对比 PyTorch 和 ONNX 的输出
# (略,确保数值差异在 1e-5 以内即可)

10.4 导出 TensorRT(GPU 加速)

1
2
3
4
5
6
7
8
9
10
11
12
13
from ultralytics import YOLO

model = YOLO('helmet_detection/yolov8s_train/weights/best.pt')

# 导出 TensorRT(需要 NVIDIA GPU + TensorRT 环境)
model.export(
format='engine',
imgsz=640,
half=True, # FP16 半精度(速度翻倍,精度几乎无损)
device=0,
)

# 导出的文件: best.engine

⚠️ TensorRT 注意事项

  • 需要 NVIDIA GPU + CUDA + TensorRT 环境
  • 导出的 .engine 文件与 GPU 型号绑定,不能跨设备使用
  • FP16 模式在大多数场景下精度损失可忽略,速度提升显著

10.5 速度对比(YOLOv8s,640×640)

格式 设备 推理速度 加速比
PyTorch FP32 RTX 3090 ~8 ms
ONNX FP32 RTX 3090 ~5 ms 1.6×
TensorRT FP32 RTX 3090 ~3 ms 2.7×
TensorRT FP16 RTX 3090 ~1.5 ms 5.3×
ONNX FP32 CPU (i7) ~80 ms

11. 部署:从本地到 Web 应用

11.1 本地 Python 推理服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import cv2
import numpy as np
from ultralytics import YOLO
import time

class DetectionService:
"""目标检测服务"""
def __init__(self, model_path, conf_threshold=0.5, iou_threshold=0.45):
self.model = YOLO(model_path)
self.conf_threshold = conf_threshold
self.iou_threshold = iou_threshold

def detect_image(self, image_path):
"""检测单张图片"""
results = self.model(
image_path,
conf=self.conf_threshold,
iou=self.iou_threshold,
)

detections = []
for box in results[0].boxes:
detections.append({
'class': self.model.names[int(box.cls[0])],
'confidence': float(box.conf[0]),
'bbox': [round(x, 2) for x in box.xyxy[0].tolist()],
})

return detections

def detect_video(self, video_path, output_path=None):
"""检测视频"""
cap = cv2.VideoCapture(video_path)

# 获取视频信息
fps = int(cap.get(cv2.CAP_PROP_FPS))
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

writer = None
if output_path:
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))

frame_count = 0
total_time = 0

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

start = time.time()
results = self.model(frame, conf=self.conf_threshold, verbose=False)
elapsed = time.time() - start
total_time += elapsed
frame_count += 1

# 绘制检测框
annotated = results[0].plot()

# 显示 FPS
current_fps = 1.0 / elapsed if elapsed > 0 else 0
cv2.putText(annotated, f'FPS: {current_fps:.1f}', (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)

if writer:
writer.write(annotated)

cv2.imshow('Detection', annotated)
if cv2.waitKey(1) & 0xFF == ord('q'):
break

cap.release()
if writer:
writer.release()
cv2.destroyAllWindows()

avg_fps = frame_count / total_time if total_time > 0 else 0
print(f"平均 FPS: {avg_fps:.1f}")

def detect_webcam(self, camera_id=0):
"""实时摄像头检测"""
cap = cv2.VideoCapture(camera_id)

while True:
ret, frame = cap.read()
if not ret:
break

results = self.model(frame, conf=self.conf_threshold, verbose=False)
annotated = results[0].plot()

cv2.imshow('Real-time Detection', annotated)
if cv2.waitKey(1) & 0xFF == ord('q'):
break

cap.release()
cv2.destroyAllWindows()

# 使用
service = DetectionService('best.pt', conf_threshold=0.5)
detections = service.detect_image('test.jpg')
for d in detections:
print(f"{d['class']}: {d['confidence']:.2f} @ {d['bbox']}")

11.2 FastAPI Web 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# app.py
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import JSONResponse, StreamingResponse
import uvicorn
import cv2
import numpy as np
from ultralytics import YOLO
from io import BytesIO
import time

app = FastAPI(title="目标检测 API")
model = None

@app.on_event("startup")
async def load_model():
global model
model = YOLO('best.pt')

@app.post("/detect")
async def detect(file: UploadFile = File(...), conf: float = 0.5):
"""上传图片进行目标检测"""
# 读取图片
contents = await file.read()
nparr = np.frombuffer(contents, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

if img is None:
return JSONResponse(status_code=400, content={"error": "无效的图片"})

# 检测
start = time.time()
results = model(img, conf=conf, verbose=False)
elapsed = time.time() - start

# 解析结果
detections = []
for box in results[0].boxes:
detections.append({
"class": model.names[int(box.cls[0])],
"confidence": round(float(box.conf[0]), 4),
"bbox": [round(x, 2) for x in box.xyxy[0].tolist()],
})

return {
"detections": detections,
"count": len(detections),
"inference_time_ms": round(elapsed * 1000, 2),
"image_size": {"width": img.shape[1], "height": img.shape[0]},
}

@app.post("/detect/visualize")
async def detect_visualize(file: UploadFile = File(...), conf: float = 0.5):
"""上传图片返回标注后的图片"""
contents = await file.read()
nparr = np.frombuffer(contents, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

results = model(img, conf=conf, verbose=False)
annotated = results[0].plot()

# 编码返回
_, buffer = cv2.imencode('.jpg', annotated)
return StreamingResponse(BytesIO(buffer.tobytes()), media_type="image/jpeg")

@app.get("/health")
async def health():
return {"status": "ok", "model": "YOLOv8"}

if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
1
2
3
4
5
6
7
8
9
10
# 启动服务
python app.py

# 测试 API
curl -X POST "http://localhost:8000/detect" \
-F "file=@test_image.jpg" \
-F "conf=0.5"

# 访问文档
# http://localhost:8000/docs

11.3 前端页面(简易版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>目标检测 Demo</title>
<style>
body { font-family: Arial; max-width: 800px; margin: 50px auto; padding: 20px; }
.upload-area { border: 2px dashed #ccc; padding: 40px; text-align: center; border-radius: 10px; }
.result { margin-top: 20px; }
#preview { max-width: 100%; }
.detection { background: #f5f5f5; padding: 10px; margin: 5px 0; border-radius: 5px; }
</style>
</head>
<body>
<h1>🔍 目标检测 Demo</h1>

<div class="upload-area">
<input type="file" id="fileInput" accept="image/*">
<p>选择图片或拖拽到此处</p>
</div>

<div class="result">
<img id="preview" style="display:none;">
<div id="detections"></div>
<p id="stats"></p>
</div>

<script>
const API_URL = 'http://localhost:8000';

document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;

// 显示预览
const preview = document.getElementById('preview');
preview.src = URL.createObjectURL(file);
preview.style.display = 'block';

// 发送检测请求
const formData = new FormData();
formData.append('file', file);

const response = await fetch(`${API_URL}/detect`, {
method: 'POST',
body: formData
});

const result = await response.json();

// 显示结果
const div = document.getElementById('detections');
div.innerHTML = result.detections.map(d =>
`<div class="detection">
<strong>${d.class}</strong> —
置信度: ${(d.confidence * 100).toFixed(1)}% —
位置: [${d.bbox.join(', ')}]
</div>`
).join('');

document.getElementById('stats').textContent =
`检测到 ${result.count} 个目标 | 耗时 ${result.inference_time_ms} ms`;
});
</script>
</body>
</html>

11.4 Docker 部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Dockerfile
FROM python:3.10-slim

WORKDIR /app

# 安装系统依赖
RUN apt-get update && apt-get install -y \
libgl1-mesa-glx libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*

# 安装 Python 依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制代码和模型
COPY app.py .
COPY best.pt .

EXPOSE 8000

CMD ["python", "app.py"]
1
2
3
4
5
# requirements.txt
ultralytics>=8.0
fastapi>=0.100
uvicorn>=0.23
python-multipart>=0.0.6
1
2
3
4
5
6
7
8
# 构建镜像
docker build -t yolo-detector .

# 运行(CPU)
docker run -p 8000:8000 yolo-detector

# 运行(GPU,需要 nvidia-docker)
docker run --gpus all -p 8000:8000 yolo-detector

12. 总结与进阶路线

三部曲回顾

篇章 核心内容 你掌握了什么
第一篇 图像处理基础 OpenCV 操作像素、滤波、边缘检测、特征提取
第二篇 深度学习与 CNN 神经网络原理、PyTorch 实战、迁移学习
第三篇 目标检测 YOLO 算法、训练自定义检测器、部署上线

进阶路线图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
入门(你在这里)

├─→ 实例分割:YOLOv8-seg / Mask R-CNN
│ 精确定义每个像素属于哪个物体实例

├─→ 3D 视觉:点云处理 / 单目深度估计
│ 从 2D 图片理解 3D 世界

├─→ 视频理解:目标跟踪 / 行为识别
│ 时序信息 + 空间信息

├─→ Vision Transformer:ViT / Swin Transformer / DINO
│ 用 Transformer 替代 CNN,新的范式

├─→ 多模态:CLIP / BLIP / LLaVA
│ 图像 + 文本,理解和生成

├─→ 扩散模型:Stable Diffusion / ControlNet
│ 图像生成与编辑

└─→ 具身智能:视觉 + 机器人控制
从感知到行动

推荐学习资源

课程

  • Stanford CS231n:计算机视觉深度学习经典课
  • Michigan EECS 498-007:Justin Johnson 的深度学习与 CV
  • Fast.ai Practical Deep Learning:实践导向

书籍

  • 《Deep Learning》(花书):理论基础
  • 《动手学深度学习》(d2l.ai):代码驱动
  • 《Computer Vision: Algorithms and Applications》:CV 全景

论文

  • YOLO 系列:从 v1 到 v11
  • Faster R-CNN / FPN / RetinaNet
  • DETR / Deformable DETR
  • ViT / Swin Transformer

开源项目

最后的话

计算机视觉是一个令人兴奋的领域——它让计算机获得了"看"的能力,而这种能力正在改变每一个行业。从第一篇的像素操作,到第二篇的神经网络,再到这篇的目标检测实战,你已经走完了从零到部署的完整路径。

最好的学习方式就是动手做。找一个问题,标注数据,训练模型,部署上线。你会在解决问题的过程中,学到任何教程都教不了的东西。

去创造吧。🚀


本文是「Python 计算机视觉三部曲」的最终篇。感谢阅读!