2. YOLO模型加载与推理基础

好,咱们直接进入正题。YOLO模型再厉害,也得先把它加载进来,然后跑一次前向推理,拿到输出结果。这一步是后面所有可视化和调试工作的前提。

说白了,就是三步:加载模型 → 前向推理 → 解析输出。听起来简单,但里面有几个坑,我当年刚接触时都踩过。今天咱们一个一个说清楚。

2.1 使用cv2.dnn.readNet加载YOLO模型

OpenCV的dnn模块提供了统一的接口来加载各种深度学习模型。对于YOLO,我们通常需要两个文件:

  • 权重文件(如 yolov4.weights):训练好的网络参数
  • 配置文件(如 yolov4.cfg):网络结构定义

加载代码非常简单:

import cv2

# 加载YOLO模型
net = cv2.dnn.readNet('yolov4.weights', 'yolov4.cfg')

# 可选:设置计算后端
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
我的习惯: 我一般会先检查文件是否存在,避免路径错误导致程序崩溃。另外,如果机器有GPU,可以设置DNN_TARGET_CUDA,速度能快好几倍。

这里有个细节——readNet会自动解析配置文件,识别网络结构。它支持DarkNet、TensorFlow、Caffe等多种框架。YOLOv3/v4用的是DarkNet格式,所以直接传.cfg和.weights就行。

2.2 前向推理:从图像到输出张量

模型加载好了,接下来就是把图像送进去,跑一次前向传播。这个过程分两步:

  1. 图像预处理:将图像转换成模型需要的输入格式
  2. 执行推理:调用net.forward()获取输出

看代码:

# 读取图像
image = cv2.imread('test.jpg')
height, width = image.shape[:2]

# 构建blob(图像预处理)
blob = cv2.dnn.blobFromImage(
    image, 
    scalefactor=1/255.0,      # 归一化到[0,1]
    size=(416, 416),          # YOLO输入尺寸
    mean=(0, 0, 0),           # 不减去均值
    swapRB=True,              # OpenCV是BGR,YOLO需要RGB
    crop=False
)

# 设置网络输入
net.setInput(blob)

# 前向推理
outputs = net.forward()
我曾经踩过的坑: 有一次我忘了设置swapRB=True,结果检测出来的颜色全乱了。红色物体被识别成蓝色,排查了半天才发现是通道顺序的问题。记住:OpenCV默认是BGR,而YOLO训练时用的是RGB。

blobFromImage这个函数做了好几件事:缩放、归一化、通道转换、裁剪(如果需要)。我个人习惯把size设为416×416,这是YOLOv4的默认输入尺寸。如果你用YOLOv5或更高版本,可能是640×640,记得对应调整。

2.3 理解输出张量结构

好,现在outputs拿到了。它到底是什么?

对于YOLOv4,net.forward()返回的是一个列表,里面包含三个层级的输出。每个层级对应一个检测尺度(大、中、小物体)。

每个输出张量的形状是:

[batch_size, num_boxes, 85]

其中85这个数字的含义:

索引 含义 说明
0-3 边界框坐标 cx, cy, w, h(相对于网格单元)
4 目标置信度 该位置是否有物体的概率
5-84 类别概率 COCO数据集80个类别

你想想看,每个检测框有85个值。前5个是位置和置信度,后面80个是每个类别的概率。最终检测结果就是:置信度 × 类别概率,取最大值对应的类别。

咱们用代码验证一下:

# 查看输出结构
print(f"输出层数量: {len(outputs)}")
for i, out in enumerate(outputs):
    print(f"层{i} 形状: {out.shape}")
    # 输出类似: (1, 507, 85) 或 (1, 2028, 85)

# 解析第一个检测框
first_box = outputs[0][0][0]
cx, cy, w, h = first_box[0:4]
confidence = first_box[4]
class_scores = first_box[5:]
class_id = np.argmax(class_scores)
score = class_scores[class_id]

print(f"检测框: cx={cx:.4f}, cy={cy:.4f}, w={w:.4f}, h={h:.4f}")
print(f"置信度: {confidence:.4f}")
print(f"类别ID: {class_id}, 得分: {score:.4f}")
关键点: 这里的坐标是相对于网格单元的偏移量,不是图像像素坐标。后面我们需要用cv2.dnn.NMSBoxes做非极大值抑制,并转换到原始图像尺寸。这部分下一章会详细讲。

2.4 知识结构总览

为了让你对整个流程有个直观印象,我画了一张图:

YOLO模型加载与推理流程 输入图像 blobFromImage 归一化+缩放+通道转换 readNet 加载.weights 和.cfg文件 net.forward() 输出张量 [1, N, 85] 解析输出 坐标+置信度+类别概率 注意点 • 坐标是网格相对值 • 需要做NMS去重 • 类别概率共80维 • 最终得分=置信度×类别概率

2.5 常见问题与避坑

  • 模型加载失败:检查文件路径,确认.cfg和.weights版本匹配。我遇到过用v3的cfg加载v4的weights,直接报错。
  • 推理速度慢:试试设置DNN_TARGET_CUDA,或者降低输入尺寸(比如从608降到416)。
  • 输出张量维度不对:不同YOLO版本输出结构不同。YOLOv3有3个输出层,YOLOv5只有一个。记得先print(outputs.shape)确认一下。
调试小技巧: 我经常在net.forward()之后加一句print([o.shape for o in outputs]),一眼就能看出输出结构对不对。如果形状是(1, 507, 85),说明输入是416×416;如果是(1, 2028, 85),说明是608×608。