这是「Python 计算机视觉三部曲」的最终篇。 前两篇解决了“像素处理”和“图像分类”,这一篇解决“多目标识别 + 精确定位 + 工程部署”。
你会在这篇里打通目标检测完整链路:
理解检测任务与评估体系 (IoU / NMS / mAP)
掌握 YOLO 系列关键演进逻辑 (从原理到选型)
完成从训练到导出再到上线部署的闭环
阅读导航
想先懂核心概念 :看 第 1~4 章
想尽快训练出可用模型 :看 第 5~9 章
想落地线上服务 :看 第 10~11 章
系列导航
第一篇 :计算机视觉与图像处理入门 — 从像素到特征
第二篇 :深度学习与计算机视觉 — 从 CNN 到实战
第三篇 :目标检测实战 — 从 YOLO 到部署(本文)
1. 什么是目标检测?
1.1 从分类到检测
图像分类 回答一个问题:这张图里主要有什么?
目标检测 回答两个问题:
图里有哪些物体? (分类)
每个物体在哪个位置? (定位)
输出格式:每个检测到的物体用一个边界框(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 npdef 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):.4 f} " )
2.3 锚框(Anchor Box)
锚框是预设的"参考框",模型在锚框基础上进行微调(偏移量预测),而非直接预测框的绝对坐标。
为什么需要锚框? 直接预测框坐标太难了——不同物体的尺度差异巨大。锚框提供了多个尺度和比例的"起点",模型只需学习从锚框到真实框的微调量。
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 remaining = order[1 :] ious = np.array([calculate_iou(boxes[i], boxes[j]) for j in remaining]) mask = ious < iou_threshold order = remaining[mask] return keep boxes = np.array([ [10 , 10 , 100 , 100 ], [15 , 12 , 105 , 98 ], [200 , 200 , 300 , 350 ], ]) scores = np.array([0.95 , 0.80 , 0.90 ]) keep = nms(boxes, scores, iou_threshold=0.5 ) print (f"NMS 后保留的框索引: {keep} " )
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) :如果有物体,是什么类别?
例如:目标置信度 = 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)— 经典之作
三大改进:
多尺度检测 :在 3 个不同尺度上检测,大幅提升小物体检测
Darknet-53 :引入残差连接,更深的网络
独立分类器 :用 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 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 pip install ultralytics yolo version 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 YOLOmodel = YOLO('yolov8n.pt' ) 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:.2 f} , 位置: {[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 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:.6 f} {cy:.6 f} {w:.6 f} {h:.6 f} " result = xyxy_to_yolo([100 , 50 , 300 , 400 ], 640 , 480 , class_id=0 ) print (result)
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
6.5 数据集配置文件
创建 dataset.yaml:
1 2 3 4 5 6 7 8 9 10 path: ./dataset train: images/train 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 osimport cv2from pathlib import Pathdef 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 ]: 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 YOLOmodel = YOLO('yolov8s.pt' ) results = model.train( data='helmet_dataset/dataset.yaml' , epochs=100 , imgsz=640 , batch=16 , patience=20 , lr0=0.01 , lrf=0.01 , weight_decay=0.0005 , warmup_epochs=3 , augment=True , mosaic=1.0 , mixup=0.1 , device=0 , workers=8 , project='helmet_detection' , name='yolov8s_train' , exist_ok=True , ) print (f"训练完成! 最佳 mAP50: {results['metrics/mAP50(B)' ]:.4 f} " )
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 # 验证预测可视化
7.6 推理验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from ultralytics import YOLOmodel = 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 YOLOimport jsonmodel = 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(), } detections.append(detection) 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 from ultralytics import YOLOmodel = YOLO('helmet_detection/yolov8s_train/weights/best.pt' ) metrics = model.val(data='helmet_dataset/dataset.yaml' ) print (f"mAP50: {metrics.box.map50:.4 f} " ) print (f"mAP50-95: {metrics.box.map :.4 f} " ) print (f"各类别 mAP50: {metrics.box.maps} " )
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 YOLOmodel = YOLO('yolov8s.pt' ) model.tune( data='helmet_dataset/dataset.yaml' , epochs=30 , iterations=300 , optimizer='AdamW' , plots=True , save=True , val=True , )
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 model = YOLO('yolov8s.pt' ) model.train(data='dataset.yaml' , epochs=20 , freeze=10 ) 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 ) model.train(data='dataset.yaml' , epochs=100 , mosaic=0.5 , close_mosaic=10 )
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 YOLOmodel = YOLO('helmet_detection/yolov8s_train/weights/best.pt' ) model.export( format ='onnx' , imgsz=640 , simplify=True , dynamic=False , opset=12 , )
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 onnximport onnxruntime as ortimport numpy as npimport cv2onnx_model = onnx.load('best.onnx' ) onnx.checker.check_model(onnx_model) print ("ONNX 模型检查通过!" )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} " ) 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]} " )
10.4 导出 TensorRT(GPU 加速)
1 2 3 4 5 6 7 8 9 10 11 12 13 from ultralytics import YOLOmodel = YOLO('helmet_detection/yolov8s_train/weights/best.pt' ) model.export( format ='engine' , imgsz=640 , half=True , device=0 , )
⚠️ TensorRT 注意事项 :
需要 NVIDIA GPU + CUDA + TensorRT 环境
导出的 .engine 文件与 GPU 型号绑定 ,不能跨设备使用
FP16 模式在大多数场景下精度损失可忽略,速度提升显著
10.5 速度对比(YOLOv8s,640×640)
格式
设备
推理速度
加速比
PyTorch FP32
RTX 3090
~8 ms
1×
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 cv2import numpy as npfrom ultralytics import YOLOimport timeclass 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() current_fps = 1.0 / elapsed if elapsed > 0 else 0 cv2.putText(annotated, f'FPS: {current_fps:.1 f} ' , (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:.1 f} " ) 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' ]:.2 f} @ {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 from fastapi import FastAPI, File, UploadFilefrom fastapi.responses import JSONResponse, StreamingResponseimport uvicornimport cv2import numpy as npfrom ultralytics import YOLOfrom io import BytesIOimport timeapp = 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 curl -X POST "http://localhost:8000/detect" \ -F "file=@test_image.jpg" \ -F "conf=0.5"
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 FROM python:3.10 -slimWORKDIR /app RUN apt-get update && apt-get install -y \ libgl1-mesa-glx libglib2.0-0 \ && rm -rf /var/lib/apt/lists/* 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 . docker run -p 8000:8000 yolo-detector 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 计算机视觉三部曲」的最终篇。感谢阅读!