模型加载与推理基础:cv2.dnn.readNet() 函数详解
好,咱们今天来聊聊 OpenCV DNN 模块里最基础、也最核心的一步——模型加载。
说实话,我刚开始接触 OpenCV 做深度学习部署时,也踩过不少坑。明明模型训练得好好的,一到加载就报错,或者推理结果完全不对。后来才发现,问题往往出在 cv2.dnn.readNet() 这个函数上。它就像一扇门,门没开对,后面的事全白搭。
一、cv2.dnn.readNet() 到底在干什么?
说白了,这个函数就是把别人训练好的模型文件,读进 OpenCV 的内存里,变成一个可以用的网络对象。你想想看,训练好的模型可能是 Caffe、TensorFlow、PyTorch 或者 ONNX 格式的,OpenCV 得把它们都翻译成自己能理解的内部表示。
我个人习惯把 readNet() 理解为「模型翻译官」。它不关心你模型是怎么训练的,只关心你能不能给我一个标准的网络结构。
核心函数签名:
net = cv2.dnn.readNet(model, config=None, framework=None)
- model:权重文件路径(必填)
- config:网络结构文件路径(某些框架需要)
- framework:显式指定框架类型(可选,但建议指定)
二、支持模型格式详解
这里我重点说一下四种主流格式,因为我在项目中都遇到过,每种都有它的脾气。
1. Caffe 模型(.caffemodel + .prototxt)
Caffe 是最早被 OpenCV 支持的框架之一。它需要两个文件:一个 .prototxt 描述网络结构,一个 .caffemodel 存权重。
net = cv2.dnn.readNet('bvlc_googlenet.caffemodel', 'deploy.prototxt')
嗯,这里要注意:Caffe 的 prototxt 文件里,输入层的名字和维度必须写对。我曾经遇到过 prototxt 里写的是 input: "data",但后面推理时用的 blob 名字是 "input",结果死活跑不通。
2. TensorFlow 模型(.pb 文件)
TensorFlow 的冻结图(frozen graph)是一个 .pb 文件,里面同时包含了网络结构和权重。
net = cv2.dnn.readNet('frozen_inference_graph.pb', 'graph.pbtxt')
这里有个坑:第二个参数 config 是 .pbtxt 文本文件,它描述了网络的输入输出节点。如果你没有这个文件,OpenCV 会尝试自动推断,但经常推断不准。我建议你从 TensorFlow 的导出脚本里,把 pbtxt 一起导出来。
避坑指南:我曾经在部署一个 SSD 检测模型时,忘了提供 pbtxt 文件,结果 OpenCV 把输出节点的名字搞错了,推理出来的框全跑到图像外面去了。后来老老实实把 pbtxt 加上,问题就解决了。
3. PyTorch 模型(通过 ONNX 转换)
OpenCV 不能直接读 PyTorch 的 .pth 文件。你需要先把 PyTorch 模型转成 ONNX 格式。
# PyTorch 端导出 ONNX
torch.onnx.export(model, dummy_input, 'model.onnx',
input_names=['input'], output_names=['output'])
# OpenCV 端加载
net = cv2.dnn.readNet('model.onnx')
我个人建议:导出 ONNX 时,opset_version 不要用太新的,11 或 12 比较稳妥。太新的算子 OpenCV 可能不支持。
4. ONNX 模型(.onnx 文件)
ONNX 是现在最通用的中间格式。OpenCV 对 ONNX 的支持也越来越好。
net = cv2.dnn.readNet('model.onnx')
只有一个文件,干净利落。但要注意:ONNX 模型里的输入输出名字,你得记清楚,后面推理时要用。
| 框架 | 文件组成 | readNet 参数 | 常见问题 |
|---|---|---|---|
| Caffe | .caffemodel + .prototxt | model, config | prototxt 输入层定义 |
| TensorFlow | .pb + .pbtxt(可选) | model, config | 缺少 pbtxt 导致节点名错误 |
| PyTorch | 需转 ONNX | model(ONNX 文件) | opset 版本兼容性 |
| ONNX | .onnx | model | 输入输出名字需记录 |
三、前向推理流程
模型加载完了,接下来就是推理。说白了,就是把一张图片塞进网络,让它算一遍,然后拿到结果。
完整的推理流程分四步,我一步步说。
第一步:加载模型
net = cv2.dnn.readNet('model.onnx')
这一步我们已经讲过了。加载成功后,net 就是一个 cv2.dnn_Net 对象。
第二步:准备输入数据(blobFromImage)
OpenCV 读进来的图片是 HWC 格式(高、宽、通道),但深度学习模型通常要求 CHW 格式(通道、高、宽),而且还要做归一化、缩放等预处理。
blob = cv2.dnn.blobFromImage(image, scalefactor=1.0/255.0,
size=(224, 224), mean=(0, 0, 0),
swapRB=True, crop=False)
- scalefactor:缩放因子,通常 1/255 把像素值归一化到 [0,1]
- size:模型要求的输入尺寸
- mean:减均值,用于中心化
- swapRB:OpenCV 默认 BGR 顺序,很多模型需要 RGB,所以设为 True
我个人习惯:swapRB=True 几乎总是要设的,除非你确定模型是用 BGR 训练的。
第三步:设置网络输入并前向传播
net.setInput(blob, name='input') # name 是输入层的名字
output = net.forward(name='output') # name 是输出层的名字
这里 name 参数很关键。如果你不指定,OpenCV 会用默认的第一个输入/输出。但万一模型有多个输入输出,你就得明确指定。
小技巧:如果你不确定模型输入输出的名字,可以用 net.getUnconnectedOutLayersNames() 来查看所有输出层的名字。
第四步:解析输出结果
这一步因任务而异。分类任务就是取 argmax,检测任务要解析边界框,分割任务要处理像素级结果。
# 分类任务示例
scores = output[0]
class_id = np.argmax(scores)
confidence = scores[class_id]
嗯,这里要注意:output 是一个列表,每个元素对应一个输出层。即使只有一个输出层,它也是列表形式。
四、完整示例代码
我把上面这些串起来,给你一个完整的分类推理示例。这个代码我在多个项目里都用过,基本框架不变。
import cv2
import numpy as np
# 1. 加载模型
net = cv2.dnn.readNet('model.onnx')
# 2. 读取并预处理图片
image = cv2.imread('cat.jpg')
blob = cv2.dnn.blobFromImage(image, 1.0/255.0, (224, 224),
(0, 0, 0), swapRB=True, crop=False)
# 3. 设置输入并推理
net.setInput(blob, name='input')
output = net.forward(name='output')
# 4. 解析结果
scores = output[0]
class_id = np.argmax(scores)
confidence = scores[class_id]
print(f'类别ID: {class_id}, 置信度: {confidence:.4f}')
五、常见问题与避坑
问题1:加载模型时报错 "Unknown layer type"
这说明模型里用了 OpenCV 不支持的层。解决办法:要么换模型,要么用 ONNX 格式并确保算子被支持。我曾经在部署一个用了自定义层的模型时,折腾了两天才发现 OpenCV 不支持那个层,最后只能换模型。
问题2:推理结果全是 NaN 或 0
大概率是输入预处理错了。检查一下:swapRB 设对了没?scalefactor 和 mean 跟训练时一致吗?我遇到过最离谱的一次,是忘了设 swapRB=True,结果模型把红色当蓝色,分类结果全乱套。
问题3:推理速度太慢
试试设置后端和目标设备:
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
如果有 GPU,可以换成 DNN_BACKEND_CUDA 和 DNN_TARGET_CUDA。不过要注意,CUDA 后端需要编译 OpenCV 时开启支持。
好了,关于模型加载和推理基础,我就讲这么多。你想想看,其实核心就三件事:加载、预处理、前向传播。但每一件都有细节,细节决定成败。下次你遇到模型加载失败,不妨按我上面说的思路排查一遍,大概率能找到问题。