ZH ·
🌏 English

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_32FCV_8U

返回值: 返回一个维度顺序为 NCHW 的 4 维 cv::Mat

cv::Mat 与 Blob 的区别

主要区别在于数据排列格式。在普通的 cv::Mat 图像中,数据以 HWC 格式排列(如 RGBRGB...RGB)。而转换为 Blob 后,它变成了深度学习框架常用的 NCHW 格式。

在普通的 cv::Mat 中,我们通过 colsrows 获取宽高。但在 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;
}