OpenCV DNN 批量推理:图像分类实战指南
OpenCV 的 DNN 模块功能强大、高效且易于使用。要实现深度学习推理应用,我们只需调用 OpenCV DNN 模块提供的几个核心 API。使用 OpenCV 实现 DNN 推理的基本流程如下:
- 初始化:通过读取网络权重文件(如 Caffe、ONNX 等)创建
cv::dnn::Net对象。- 预处理:确定网络输入数据的形状,并将原始输入图像调整为匹配的形状。此步骤通常包含归一化、均值减法等操作。
- 推理:利用创建的
cv::dnn::Net对象执行前向传播。- 后处理:解析输出数据并进行进一步的业务逻辑处理。
在我看来,由于网络权重在部署阶段已经确定,最重要的部分是预处理和后处理。你需要明确输入数据的精确维度以及归一化参数(均值/标准差)。后处理逻辑可能会更加复杂:分类任务相对简单,但目标检测或分割任务则需要处理大量的数据解析工作。有时你甚至需要从头实现某些操作,因为 C++ 中可能没有与原始 Python 实现完全对应的函数。
本文将介绍如何使用 OpenCV DNN 模块实现简单的图像分类,并重点讲解如何进行批量推理(Batch Inference)。
核心 API 介绍
为了将模型权重加载到设备并创建 DNN Net 对象,OpenCV 提供了 readNet 系列方法,支持 ONNX、Caffe、TensorFlow、OpenVINO 等多种格式。
本文以 Caffe 模型为例:
Net cv::dnn::readNetFromCaffe( const String & prototxt, \
const String & caffeModel = String())
参数说明:
prototxt:包含网络结构的.prototxt文件路径。caffeModel:包含训练权重的.caffemodel文件路径。返回值: 返回一个
Net对象。
此外,我们需要调用 blobFromImage 对输入的 cv::Mat 图像进行预处理。
Mat cv::dnn::blobFromImage( InputArray image,
double scalefactor = 1.0,
const Size & size = Size(),
const Scalar & mean = Scalar(),
bool swapRB = false,
bool crop = false,
int ddepth = CV_32F)
从图像创建 4 维 Blob。可选操作包括:调整大小(Resize)、中心裁剪(Crop)、减去均值(Mean Subtraction)、按比例缩放(Scale)以及交换红蓝通道(RGB/BGR 转换)。
参数说明:
image:输入图像(1、3 或 4 通道)。size:输出图像的空间尺寸。mean:从通道中减去的均值。如果swapRB为 true,则顺序应为 (R, G, B)。scalefactor:图像像素值的缩放倍数。swapRB:是否交换第一个和最后一个通道(通常用于 BGR 转 RGB)。crop:调整大小后是否执行中心裁剪。ddepth:输出 Blob 的深度,可选CV_32F或CV_8U。返回值: 返回一个维度顺序为 NCHW 的 4 维
cv::Mat。
cv::Mat 与 Blob 的区别
主要区别在于数据排列格式。在普通的 cv::Mat 图像中,数据以 HWC 格式排列(如 RGBRGB...RGB)。而转换为 Blob 后,它变成了深度学习框架常用的 NCHW 格式。
在普通的 cv::Mat 中,我们通过 cols 和 rows 获取宽高。但在 Blob 中,这两个变量通常为 -1,我们需要通过 blob.size(0)(Batch)、blob.size(1)(Channels)等来获取维度信息。这在解析批量推理的结果矩阵时非常重要。
引入依赖与初始化
首先包含必要的头文件,并声明一个全局的 cv::dnn::Net 变量。
#include <iostream>
#include <string>
#include <vector>
#include "opencv2/core.hpp"
#include "opencv2/dnn.hpp"
#include "opencv2/core/cuda.hpp"
// 全局 Net 变量
cv::dnn::Net net;
假设我们有一个 Caffe 模型,在 init 函数中执行加载工作。为了提高推理速度,我们开启了 CUDA 支持。如果你需要了解如何编译带 CUDA 的 OpenCV,请参考相关指南。
void init(const std::string& model_deploy, const std::string& model_bin)
{
net = cv::dnn::readNetFromCaffe(model_deploy, model_bin);
// 设置 CUDA 作为计算后端
cv::cuda::setDevice(cuda_id);
this->net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
this->net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
}
单图像推理
单图推理非常直观:调用 blobFromImage 将图像转换为 Blob,通过 net.setInput 设置输入,最后调用 net.forward() 获取结果。
void single_inference(const cv::Mat& image)
{
if (image.empty())
{
std::cout << "Empty image!!!" << std::endl;
return;
}
// 预处理:缩放至 224x224
cv::Mat blob = cv::dnn::blobFromImage(image, 1.0, cv::Size(224, 224), \
cv::Scalar(0, 0, 0), false, false);
net.setInput(blob);
// 执行推理
cv::Mat out = net.forward();
post_process(out);
}
后处理部分,我们假设模型的最后一层是 Softmax。我们使用 cv::minMaxLoc 找到最大概率对应的索引(即类别标签)。
void post_process(const cv::Mat& out)
{
double min_val;
double max_val;
cv::Point min_pos;
cv::Point max_pos;
cv::minMaxLoc(out, &min_val, &max_val, &min_pos, &max_pos);
int label = max_pos.x;
std::cout << "Result label: " << label << " with score: " << max_val << std::endl;
}
批量推理 (Batch Inference)
批量推理允许在一次前向传播中处理多张图像,这通常能更充分地利用 GPU 算力。我们使用 blobFromImages 方法。
void batch_inference(const std::vector<cv::Mat>& images)
{
// 将图像列表转换为 NCHW Blob
cv::Mat blob = cv::dnn::blobFromImages(images, 1.0, cv::Size(224, 224), cv::Scalar(0, 0, 0), false, false);
net.setInput(blob);
cv::Mat out = net.forward();
// 解析输出:每一行对应一张图的推理结果
for (int i = 0; i < out.rows; ++i)
{
cv::Mat out_single = out.rowRange(i, i + 1);
post_process(out_single);
}
}
完整示例代码
以下是一个完整的实现示例,演示了如何加载 ImageNet Caffe 模型并对多张本地图片进行推理。你可以从 这里 下载示例模型。
#include <iostream>
#include <string>
#include <vector>
#include "opencv2/core.hpp"
#include "opencv2/dnn.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
typedef std::pair<int, float> Result;
Result post_process(const cv::Mat& out)
{
double min_val, max_val;
cv::Point min_loc, max_loc;
cv::minMaxLoc(out, &min_val, &max_val, &min_loc, &max_loc);
return std::make_pair(max_loc.x, (float)max_val);
}
std::vector<Result> batch_post_process(const cv::Mat& outs)
{
std::vector<Result> ret;
for(int i = 0; i < outs.rows; i++)
{
cv::Mat out = outs.rowRange(i, i + 1);
auto result = post_process(out);
ret.push_back(result);
}
return ret;
}
int main()
{
// 加载网络
cv::dnn::Net net = cv::dnn::readNetFromCaffe("models/deploy.prototxt", "models/resnet50_cvgj_iter_320000.caffemodel");
std::vector<std::string> image_files = {"data/a.jpeg", "data/b.jpeg", "data/c.jpeg"};
std::vector<cv::Mat> images;
for (const auto& file : image_files)
{
cv::Mat img = cv::imread(file);
if (!img.empty()) images.push_back(img);
}
if (images.empty()) return -1;
// 预处理
cv::Mat blobs = cv::dnn::blobFromImages(images, 1.0, cv::Size(224, 224), cv::Scalar(104, 117, 123), false, false);
// 推理
net.setInput(blobs);
cv::Mat outs = net.forward();
// 后处理与可视化
auto results = batch_post_process(outs);
for (size_t i = 0; i < results.size(); ++i)
{
auto result = results[i];
std::cout << "Image " << i << " -> Label: " << result.first << " Score: " << result.second << std::endl;
char result_text[50];
sprintf(result_text, "ID:%d Score:%.2f", result.first, result.second);
cv::putText(images[i], result_text, cv::Point(10, 30), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 255), 2);
cv::imshow("Inference Result", images[i]);
cv::waitKey(0);
}
return 0;
}