4、OpenCV DNN模块基础:cv2.dnn.readNet、设置后端与目标设备、前向推理、获取输出层

说实话,很多做视觉的朋友一开始接触深度学习推理,第一反应就是装PyTorch或者TensorFlow。但如果你只是想把训练好的模型用起来,尤其是部署到嵌入式设备或者现有C++项目里,OpenCV的DNN模块其实是个被低估的利器。

我个人习惯把DNN模块叫做“轻量级推理引擎”。它不需要额外的深度学习框架依赖,一个OpenCV库就搞定了。今天我们就把它最核心的几个操作捋清楚。

4.1 加载模型:cv2.dnn.readNet

这是所有工作的起点。你训练好的模型,不管是什么框架出来的,最终都要通过这个函数读进来。

import cv2

# 加载Caffe模型
net = cv2.dnn.readNet('deploy.prototxt', 'model.caffemodel')

# 加载TensorFlow模型
net = cv2.dnn.readNet('frozen_graph.pb', 'graph.pbtxt')

# 加载ONNX模型(推荐)
net = cv2.dnn.readNet('model.onnx')

这里有个细节——ONNX模型只需要一个文件,其他框架通常需要两个(结构文件+权重文件)。我在项目里踩过坑:有一次同事给了个.pb文件,我死活加载不上,后来发现他忘了给pbtxt。从那以后,我统一要求团队导出ONNX格式,省心很多。

我的建议: 新项目尽量用ONNX。它跨框架兼容性好,而且OpenCV对ONNX的支持越来越完善。YOLOv8官方导出的就是ONNX,直接readNet就能读。

4.2 设置后端与目标设备

模型加载完了,接下来要告诉OpenCV:你用CPU算还是GPU算?用OpenCV自己的实现还是用第三方库加速?

后端(Backend)决定计算引擎,目标设备(Target)决定计算位置。这两者是正交的,可以自由组合。

后端 说明 适用场景
cv2.dnn.DNN_BACKEND_OPENCV OpenCV自带实现 CPU推理,通用兼容
cv2.dnn.DNN_BACKEND_INFERENCE_ENGINE Intel OpenVINO Intel CPU/VPU加速
cv2.dnn.DNN_BACKEND_CUDA NVIDIA CUDA GPU推理,性能最高
cv2.dnn.DNN_BACKEND_TIM_VX TI处理器 嵌入式TI平台
# 设置CPU后端
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)

# 设置CUDA GPU后端(需要编译时开启)
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
注意: CUDA后端需要你在编译OpenCV时开启WITH_CUDA和WITH_CUDNN。我曾经在客户现场调试了半天,发现他装的OpenCV是官方预编译版,根本不支持CUDA。后来重新编译才解决。

你想想看,如果只是做原型验证,CPU后端完全够用。但一旦要上生产线,GPU加速几乎是必须的。我测过一个YOLOv8s模型,在RTX 3060上用CUDA后端,推理时间从CPU的120ms降到了8ms——15倍的差距。

4.3 前向推理:net.forward

模型加载好了,设备设置好了,接下来就是真正的推理。但在这之前,你得先把输入图像处理成模型能吃的格式。

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

# 构建blob(批量+归一化+缩放)
blob = cv2.dnn.blobFromImage(
    img, 
    scalefactor=1.0/255.0,  # 归一化
    size=(640, 640),         # 输入尺寸
    mean=(0, 0, 0),          # 均值
    swapRB=True,             # BGR -> RGB
    crop=False
)

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

# 前向推理
outputs = net.forward()

这里有个容易搞混的地方——blobFromImage的参数顺序。我刚开始用的时候,总把mean和scalefactor搞反。记住一个口诀:先缩放,再减均值。实际计算是 (pixel * scalefactor) - mean。

关键点: blobFromImage返回的是一个4维张量,形状是 (batch, channels, height, width)。对于单张图片,batch=1。这个格式和PyTorch的NCHW是一致的。

4.4 获取输出层

模型推理完了,输出结果怎么拿?这取决于你的模型结构。对于YOLO这类多尺度检测模型,输出层通常不止一个。

# 获取所有输出层的名称
layer_names = net.getLayerNames()
output_layers = [layer_names[i - 1] for i in net.getUnconnectedOutLayers()]

# 指定输出层进行推理
outputs = net.forward(output_layers)

getUnconnectedOutLayers()返回的是输出层的索引ID,减1是因为OpenCV内部索引从1开始。这个细节坑过不少人,包括我。第一次用的时候,直接拿索引去取名字,结果取错了层,检测结果全是乱的。

拿到输出后,对于YOLO模型,每个输出层是一个三维张量:(batch, num_boxes, 85)。85的含义是:4个坐标 + 1个置信度 + 80个类别概率(COCO数据集)。

4.5 完整流程与知识图谱

把上面几个步骤串起来,就是DNN模块推理的标准流水线。我画了一张图帮你理清关系:

OpenCV DNN 推理流水线 cv2.dnn.readNet setPreferableBackend setPreferableTarget blobFromImage net.setInput net.forward getUnconnectedOutLayers 解析检测结果 💡 关键顺序: readNet → 设置后端/设备 → blobFromImage → setInput → forward → 获取输出层 → 后处理

4.6 完整代码示例

最后,给你一个可以直接跑的完整示例。这个代码加载YOLOv8 ONNX模型,对一张图片做推理:

import cv2
import numpy as np

# 1. 加载模型
net = cv2.dnn.readNet('yolov8n.onnx')

# 2. 设置后端(有CUDA就用CUDA,没有就用CPU)
try:
    net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
    net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
    print("使用CUDA GPU加速")
except:
    net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
    net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
    print("使用CPU推理")

# 3. 读取并预处理图像
img = cv2.imread('test.jpg')
blob = cv2.dnn.blobFromImage(img, 1.0/255.0, (640, 640), swapRB=True)

# 4. 设置输入并推理
net.setInput(blob)
outputs = net.forward()

# 5. 获取输出层名称(调试用)
layer_names = net.getLayerNames()
output_layers = [layer_names[i - 1] for i in net.getUnconnectedOutLayers()]
print(f"输出层: {output_layers}")

# 6. 解析结果(简化版)
print(f"输出张量形状: {outputs.shape}")
# 实际后处理需要NMS等操作,后续章节会详细讲
我的经验: 调试阶段建议把net.forward()改成net.forward(output_layers),这样能明确知道每个输出层对应什么。我曾经因为没指定输出层,拿到的结果维度不对,排查了半天才发现是多层输出被拼接了。