模型加载与推理基础: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 设对了没?scalefactormean 跟训练时一致吗?我遇到过最离谱的一次,是忘了设 swapRB=True,结果模型把红色当蓝色,分类结果全乱套。

问题3:推理速度太慢

试试设置后端和目标设备:

net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)

如果有 GPU,可以换成 DNN_BACKEND_CUDADNN_TARGET_CUDA。不过要注意,CUDA 后端需要编译 OpenCV 时开启支持。

好了,关于模型加载和推理基础,我就讲这么多。你想想看,其实核心就三件事:加载、预处理、前向传播。但每一件都有细节,细节决定成败。下次你遇到模型加载失败,不妨按我上面说的思路排查一遍,大概率能找到问题。