OpenCV DNN 实战教程:PyTorch 图像分类模型的 ONNX 转换与 C++ 推理完整流程
【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv
本文以 OpenCV 官方教程 PyTorch 分类模型转换 C++ 实践 为主体,完整讲解“PyTorch 分类模型 → ONNX → cv::dnn 网络 → C++ 推理”的全流程:如何用torch.onnx.export把 torchvision 中的 ResNet-50 等 17 种分类模型导出为 ONNX,再用cv::dnn::readNetFromONNX加载并推理。读完后你将能够:独立搭建模型转换环境、复用仓库自带的转换脚本批量导出模型、理解 OpenCV 预处理参数(mean/scale/std/crop)与 PyTorch 原始预处理顺序的对应关系,并用 classification.cpp 示例程序对任意 ONNX 分类模型跑通推理。
1. 转换流水线概览
教程给出的核心链路只有三步:
- PyTorch 模型 → ONNX:使用 PyTorch 内置的
torch.onnx.export。ONNX 的目标是实现神经网络在不同框架间的互换性,因此它是 PyTorch 模型进入 OpenCV 的“中间格式”; - ONNX → cv::dnn::Net:将
.onnx文件交给cv::dnn::readNetFromONNX或通用的cv::dnn::readNet解析为内部计算图; - 推理:通过
setInput/forward完成前向计算并解析输出。
整个教程以ResNet-50为例走通上述链路。
2. 环境准备与依赖安装
教程要求使用Python 3.7+的虚拟环境:
virtualenv -p /usr/bin/python3.7 <env_dir_path> source <env_dir_path>/bin/activate如果要用 OpenCV-Python(即cv2.dnn)从源码构建,需按 OpenCV 官方构建文档操作(见教程中的 setup 章节指引)。
依赖清单位于 requirements.txt,激活虚拟环境后执行:
pip install -r requirements.txt该文件声明的版本基线(摘自文件内容):
| 依赖 | 最低版本 | 用途 |
|---|---|---|
onnx | >=1.7.0 | ONNX 工具链 |
numpy | >=1.19.1 | 数值计算 |
torch/torchvision | >=1.5.1 / >=0.6.1 | 模型实例化与 ONNX 导出 |
tensorflow | >=2.1.0 | 同目录下的 TF 模型转换脚本共用 |
paddlepaddle系列 | >=2.0.0 | PaddlePaddle 模型转换脚本共用 |
说明:该文件是转换工具目录(dnn_conversion/)的公共依赖,PyTorch / TensorFlow / PaddlePaddle 三条转换链路共用。如果只做 PyTorch 分类模型转换,教程明确提示可以先编辑该文件,剔除 tensorflow、paddle 相关条目(
opencv-python可视情况保留),只装onnx、numpy、torch、torchvision。
3. 模型转换流水线
3.1 最小可运行示例:ResNet-50 导出
代码位于 samples/dnn/dnn_model_runner/dnn_conversion/pytorch/classification/py_to_py_resnet50_onnx.py,从samples/dnn目录执行:
python -m dnn_model_runner.dnn_conversion.pytorch.classification.py_to_py_resnet50_onnx它只做两件事——实例化模型、导出 ONNX:
# initialize PyTorch ResNet-50 model original_model = models.resnet50(pretrained=True) # get the path to the converted into ONNX PyTorch model full_model_path = get_pytorch_onnx_model(original_model) print("PyTorch ResNet-50 model was successfully converted: ", full_model_path)核心转换逻辑get_pytorch_onnx_model(...)完全基于torch.onnx.export:
# define the directory for further converted model save onnx_model_path = "models" # define the name of further converted model onnx_model_name = "resnet50.onnx" # create directory for further converted model os.makedirs(onnx_model_path, exist_ok=True) # get full path to the converted model full_model_path = os.path.join(onnx_model_path, onnx_model_name) # generate model input generated_input = Variable( torch.randn(1, 3, 224, 224) ) # model export into ONNX format torch.onnx.export( original_model, generated_input, full_model_path, verbose=True, input_names=["input"], output_names=["output"], opset_version=11 )各参数含义:
generated_input:构造一个1x3x224x224的随机张量作为“轨迹输入”,导出器沿前向轨迹固化图结构,同时确定输入形状(batch=1、三通道、224×224);input_names/output_names:把逻辑输入/输出命名成input/output,方便后续setInput按名字寻址;opset_version=11:指定 ONNX 算子集版本,决定导出图中允许使用的算子;verbose=True:打印导出过程信息,便于排查不支持的算子。
成功执行后的输出:
PyTorch ResNet-50 model was successfully converted: models/resnet50.onnx3.2 通用转换脚本:17 种模型一键导出
py_to_py_cls.py 把上述步骤泛化为命令行工具,支持 torchvision 中的整套分类网络:
- alexnet
- vgg11 / vgg13 / vgg16 / vgg19
- resnet18 / resnet34 / resnet50 / resnet101 / resnet152
- squeezenet1_0 / squeezenet1_1
- resnext50_32x4d / resnext101_32x8d
- wide_resnet50_2 / wide_resnet101_2
转换(不含与 PyTorch 原模型的精度评估)只需:
python -m dnn_model_runner.dnn_conversion.pytorch.classification.py_to_py_cls --model_name <pytorch_cls_model_name> --evaluate False以 ResNet-50 为例:
python -m dnn_model_runner.dnn_conversion.pytorch.classification.py_to_py_cls --model_name resnet50 --evaluate False从源码看,--model_name通过 utils.py 中的create_extended_parser做白名单校验,传错名字会直接报错并打印可选列表;--evaluate默认是 True,置为 False 时只走“导出 + 用 OpenCV 测试推理”的链路。
转换产物保存位置由 test_config.py 的CommonConfig定义:
@dataclass class CommonConfig: output_data_root_dir: str = "dnn_model_runner/dnn_conversion"即模型默认落在dnn_model_runner/dnn_conversion之下。结合 utils.py 中的路径拼接逻辑(MODEL_PATH_ROOT = output_data_root_dir + "/{库名}/models",库名取pytorch)可以推断,通用脚本实际把转换结果写到dnn_model_runner/dnn_conversion/pytorch/models/<model_name>.onnx;而 3.1 节的最小示例脚本是相对工作目录直接写入models/resnet50.onnx。
值得一提的是,通用脚本内部复用的PyTorchModelPreparer(pytorch_model.py)与最小示例完全同构:同样的torch.randn(batch, 3, height, width)轨迹输入、同样的opset_version=11,导出完成后还会立即调用cv2.dnn.readNetFromONNX回读一次,等于在 Python 侧先做了一次加载自检,再进入与 PyTorch 原模型的对比评测(PyTorchModelProcessorvsPyTorchDnnModelProcessor)。
4. C++ 推理流水线:example_dnn_classification
4.1 构建与运行
转换得到的models/resnet50.onnx即可交给 OpenCV C++ API。推理示例即 samples/dnn/classification.cpp,构建时把BUILD_EXAMPLES置为ON,编译后得到可执行文件example_dnn_classification。
教程使用 samples/data/squirrel_cls.jpg 这张松鼠照片(对应 ImageNet ILSVRC2012 类别 335:“fox squirrel, eastern fox squirrel, Sciurus niger”),并需要类别名文件 samples/data/dnn/classification_classes_ILSVRC2012.txt(包含全部 1000 个 ImageNet 类名)做标签解码。从构建目录(samples/build)执行:
./dnn/example_dnn_classification --model=../dnn/models/resnet50.onnx --input=../data/squirrel_cls.jpg --width=224 --height=224 --rgb=true --scale="0.003921569" --mean="123.675 116.28 103.53" --std="0.229 0.224 0.225" --crop=true --initial_width=256 --initial_height=256 --classes=../data/dnn/classification_classes_ILSVRC2012.txt4.2 参数逐项解读
结合 common.hpp 中genPreprocArguments生成的参数定义,各参数含义与默认值如下:
| 参数 | 默认值 | 含义 |
|---|---|---|
--model | 空 | ONNX 模型文件路径(必填,否则CV_Assert失败) |
--input | 空 | 输入图像/视频;不给则打开默认摄像头 |
--width/--height | -1 | blobFromImage的目标尺寸(-1 表示不 resize) |
--scale | 1.0 | 像素值缩放系数 |
--mean | 无 | 减去的均值,BGR 顺序、空格分隔 |
--std | 无 | 除的标准差 |
--rgb | false | 模型是否吃 RGB(内部把 BGR 交换为 RGB) |
--crop | false | 是否中心裁剪到目标尺寸 |
--labels | 无 | 类别名文本文件路径 |
--backend/--target | default/cpu | 计算后端(openvino、opencv、cuda、webnn…)与目标设备(opencl、vulkan、cuda_fp16…),由 common.hpp 中的映射表解析 |
注意 findFile / findModel 的寻址规则:相对路径找不到时,会依次尝试环境变量OPENCV_SAMPLES_DATA_PATH(指向opencv/samples/data)、OPENCV_DNN_TEST_DATA_PATH、OPENCV_DOWNLOAD_CACHE_DIR下的同名文件。教程示例中的../data/squirrel_cls.jpg正是利用“相对构建目录 + 数据根目录”的方式定位素材。
4.3 源码关键步骤解析
classification.cpp的推理主循环(L148-L259)可拆成四步,与教程逐一对应:
1)加载模型并初始化网络
教程展示的是通用写法Net net = readNet(model, config, framework);。当前仓库源码更直接:
//! [Read and initialize network] EngineType engine = ENGINE_OPENCV; Net net = readNetFromONNX(model, engine); net.setPreferableBackend(getBackendID(backend)); net.setPreferableTarget(getTargetID(target)); net.setProfilingMode(DNN_PROFILE_SUMMARY); //! [Read and initialize network]readNetFromONNX是readNet针对 ONNX 容器的专门入口,--model指向的resnet50.onnx在此被解析为 OpenCV 内部的cv::dnn::Net;同时通过setProfilingMode(DNN_PROFILE_SUMMARY)打开性能摘要,每帧末尾net.printPerfProfile()会打印各层耗时。
2)输入预处理:blobFromImage + 除以 std
blobFromImage(frame, blob, scale, Size(inpWidth, inpHeight), mean, swapRB, crop); // Check std values. if (std.val[0] != 0.0 && std.val[1] != 0.0 && std.val[2] != 0.0) { // Divide blob by std. divide(blob, std, blob); }这里是“复现 PyTorch 预处理”的关键。cv::dnn::blobFromImage的执行顺序是先减 mean、再乘 scale,且没有内置“除以 std”的步骤,所以示例把除法放到外面。而 PyTorch ResNet 的标准预处理顺序是:
img /= 255.0 img -= [0.485, 0.456, 0.406] img /= [0.229, 0.224, 0.225]两套顺序不同,参数就需要“换一种写法”来对齐:把 mean 写成[0.485, 0.456, 0.406] × 255.0即123.675 116.28 103.53(255 尺度),scale 写成1/255 ≈ 0.003921569,std 保持0.229 0.224 0.225。这样blobFromImage内的“减 123.675 再除 255”恰好等价于“先除 255 再减 0.485”,再手动除以 std,三步结果与 PyTorch 完全一致。
此外,--crop=true配合教程给出的 resize 参数(先把短边放大到 256 再中心裁剪 224)复现了 PyTorch Hub 的Resize(256) + CenterCrop(224);--rgb=true负责 BGR→RGB 交换。仓库里 default_preprocess_config.py 中的pytorch_resize_input_blob配置(mean/scale/std/crop/rgb/256)正是同一组参数在 Python 测试管线中的镜像。作为对照,models.yml 里resnet条目给出另一种等价写法:scale: 1.0、std: [58.395, 57.12, 57.375](即0.229×255等),二者数学上等价,只是把 255 的缩放并入了 std。
3)前向传播
net.setInput(blob); Mat prob = net.forward();prob是 1×1000 的分数矩阵(ONNX 图输出名output对应的节点)。
4)后处理:取最高分类别
教程展示minMaxLoc取 argmax:
Point classIdPoint; double confidence; minMaxLoc(prob.reshape(1, 1), 0, &confidence, 0, &classIdPoint); int classId = classIdPoint.x;当前仓库实现改为对分数排序取Top-5并逐行绘制到画面上(L236-L259),标签从--labels文件按classId下标取出;同时用TickMeter统计单次推理耗时,输出Inference time: xx ms。程序在 GUI 窗口中循环显示,按q或ESC退出;对输入为图片列表(--imglist)的场景,每张图停留 1 秒自动切换。
4.4 预期结果
对squirrel_cls.jpg运行后,预测类别 ID 为335,对应标签fox squirrel, eastern fox squirrel, Sciurus niger,与教程给出的输出截图一致(见文首第二张图)。
5. 小结与延伸阅读
整条链路可以概括为一句话:用torch.onnx.export固定“图结构 + 输入形状 + 输入输出命名”,用cv::dnn::readNetFromONNX换运行时,再用“mean×255 / scale=1/255 / 显式除 std / 256→224 中心裁剪 / RGB”这组参数把 OpenCV 侧预处理对齐到 PyTorch 的官方顺序。仓库中与本主题直接相关的入口:
- 转换脚本(PyTorch 分类):py_to_py_resnet50_onnx.py、py_to_py_cls.py、py_to_py_resnet50.py(Python 侧端到端对照:同一张预处理后的图分别喂给 OpenCV 与 PyTorch,打印两边的类别与置信度)
- C++ 推理示例:classification.cpp,参数定义见 common.hpp
- 模型参数注册表:models.yml(
resnet/squeezenet等分类条目) - 素材与标签:squirrel_cls.jpg、classification_classes_ILSVRC2012.txt
- 原教程文档:pytorch_cls_model_conversion_c_tutorial.md,同目录另有 PyTorch 分类模型 Python 版教程 与 TensorFlow 对应教程,可作为姊妹篇参考
适用前提提醒:转换脚本依赖较新的 torch/onnx 版本(requirements 基线为 torch>=1.5.1、onnx>=1.7.0),torch.autograd.Variable的用法在新版 PyTorch 中已被torch.no_grad()下直接传张量取代,但导出行为不变;C++ 侧则要求 OpenCV 启用 ONNX 解析器(DNN 模块默认支持 ONNX),示例程序需BUILD_EXAMPLES=ON参与构建。
【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考