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格式,省心很多。
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)
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模块推理的标准流水线。我画了一张图帮你理清关系:
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),这样能明确知道每个输出层对应什么。我曾经因为没指定输出层,拿到的结果维度不对,排查了半天才发现是多层输出被拼接了。