C++中实现AI推理前的图像预处理_PPYOLOv2模型

在《OpenVINO获取模型输入节点信息》获得了如下结论:图像的Resize、Normalize、transpose、expand等预处理功能需要手动写一个preprocess函数来实现。

在Python中,由于存在强大的Numpy工具包,使得数组的切片、访问、广播都很容易实现,如下面代码所示:

def preprocess(self, image_file):
        """
        输入的图像进行预处理,包括resize, 归一化
        并返回模型所需的3个输入
        image: 预处理后的图像数据
        im_shape: 预处理后的图像大小
        scale_factor: 原图在处理后,分别在高和宽上的缩放系数
        """
        import cv2

        def resize(im, height, width, interp=cv2.INTER_CUBIC):
            scale_h = height / float(im.shape[0])
            scale_w = width / float(im.shape[1])
            im = cv2.resize(
                im, None, None, fx=scale_w, fy=scale_h, interpolation=interp)
            return im, scale_h, scale_w

        def normalize(im, mean, std, is_scale=True):
            if is_scale:
                im = im / 255.0
            im -= mean
            im /= std
            return im

        im = cv2.imread(image_file)
        if im is None:
            raise Exception("Can not read image file: {} by cv2.imread".format(
                image_file))
        im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
        im, scale_h, scale_w = resize(im, self.model_input_shape[0],
                                      self.model_input_shape[1])
        im = normalize(im, self.mean, self.std)

        # 数据格式由HWC转为NCHW
        im = im.transpose((2, 0, 1))
        im = np.expand_dims(im, axis=0)

但是,在C++中,没有Numpy一样的工具包,访问OpenCV Mat对象必须依赖指针,或者按像素处理,这就使得涉及维度的预处理操作格外复杂,例如,没有数组广播操作的C++,要归一化图像,必须手动处理每个像素,如下所示。这样的代码开发,显然过于复杂。

void NormalizeImage::Run(cv::Mat* im, ImageBlob* data) {
  double e = 1.0;
  if (is_scale_) {
    e /= 255.0;
  }
  (*im).convertTo(*im, CV_32FC3, e);
  for (int h = 0; h < im->rows; h++) {
    for (int w = 0; w < im->cols; w++) {
      im->at(h, w)[0] =
          (im->at(h, w)[0] - mean_[0]) / scale_[0];
      im->at(h, w)[1] =
          (im->at(h, w)[1] - mean_[1]) / scale_[1];
      im->at(h, w)[2] =
          (im->at(h, w)[2] - mean_[2]) / scale_[2];
    }
  }
}

另外,将模型输入格式由HWC转换为NCHW的操作,即先transpose,再expand,Python 中只需两行代码,如下所示:

 # 数据格式由HWC转为NCHW
        im = im.transpose((2, 0, 1))
        im = np.expand_dims(im, axis=0)

但C++中,需要中指针来重排数据,非常麻烦,如下所示:

    Mat RGBImg, ResizeImg;
    cvtColor(MatBGRImage, RGBImg, COLOR_BGR2RGB);
    cv::resize(RGBImg, ResizeImg, Size(224, 224));
    // mean_rgb = [0.485, 0.456, 0.406]
    // std_rgb  = [0.229, 0.224, 0.225]

    int channels = ResizeImg.channels(), height = ResizeImg.rows, width = ResizeImg.cols;

    float* nchwMat = (float*)malloc(channels * height * width * sizeof(float));
    memset(nchwMat, 0, channels * height * width * sizeof(float));

    // Convert HWC to CHW and Normalize
    float mean_rgb[3] = {0.485, 0.456, 0.406};
    float std_rgb[3]  = {0.229, 0.224, 0.225};
    uint8_t* ptMat = ResizeImg.ptr(0);
    int area = height * width;
    for (int c = 0; c < channels; ++c)
    {
        for (int h = 0; h < height; ++h)
        {
            for (int w = 0; w < width; ++w)
            {
                int srcIdx = c * area + h * width + w;
                int divider = srcIdx / 3;  // 0, 1, 2
                for (int i = 0; i < 3; ++i)
                {
                    nchwMat[divider + i * area] = static_cast((ptMat[srcIdx] * 1.0f/255.0f - mean_rgb[i]) * 1.0f/std_rgb[i] );
                }
            }
        }
    }

为了方便AI开发者实现图像预处理,OpenCV在3.3之后的版本,提供了一个blobFromImage函数还实现图像预处理,其作用如下:

Creates 4-dimensional blob from image. Optionally resizes and crops image from center, subtract mean values, scales values by scalefactor, swap Blue and Red channels.

由上可知,blobFromImage函数的作用主要是用来对图片进行减均值和缩放,并交换蓝红通道的预处理:

  • 整体像素值减去平均值(mean)
  • 通过缩放系数(scalefactor)对图片像素值进行缩放
  • 交换蓝红通道(可选)

函数原型和参数定义,参考官网:
blobFromImage函数原型和参数定义

范例程序代码:

// OpenVINO Sample code for PPYOLOv2
#include
#include
#include

#include
#include

#include "NiVisionOpenCV.h"
#include "ocv_common.hpp"

using namespace InferenceEngine;
using namespace std;

//将OpenCV Mat对象中的图像数据传给为InferenceEngine Blob对象
void frameToBlob(const cv::Mat& frame, InferRequest::Ptr& inferRequest, const std::string& inputName) {
    /* 从OpenCV Mat对象中拷贝图像数据到InferenceEngine 输入Blob对象 */
    Blob::Ptr frameBlob = inferRequest->GetBlob(inputName);
    matU8ToBlob(frame, frameBlob); //cv::Mat object to a given Blob object
}

//配置推理计算设备,IR文件路径,图片路径,阈值和标签
string DEVICE = "CPU";
string IR_FileXML =  "D:/pd/ov_model/ppyolov2.xml";
string imageFile = "road554.png";
float confidence_threshold = 0.7; //取值0~1
vector labels = { "speedlimit","crosswalk","trafficlight","stop" }; //标签输入

int main()
{

    // --------------------------- 1. 创建Core对象 --------------------------------------
    cout << "1.Create Core Object." << endl;
    Core ie;  // 创建Core对象
    cout << "InferenceEngine: " << GetInferenceEngineVersion() << endl;//输出IE版本信息
    cout << ie.GetVersions(DEVICE) << std::endl; //输出插件版本信息, “<<”运算符重载代码在common.hpp中

    // ------------------- 2. 将模型文件载入内存 ---------------------------------------
    cout << "2.Load the Model to the Device..." <setPrecision(Precision::U8);   //设置PPYOLOv2输入节点"image"数据精度为U8,因为OpenCV读入的数据是CV_8U.
    // 其余保持默认
   
    // --------------------------- 4. 载入模型到AI推理计算设备----------------------------
    cout << "4.Load model into device..." << endl;
    ExecutableNetwork executable_network = ie.LoadNetwork(network, DEVICE); //配置模型输入节点,加载network
    //ExecutableNetwork executable_network = ie.LoadNetwork(IR_FileXML, DEVICE); //不配置模型输入节点,直接加载

    // --------------------------- 5. 创建Infer Request------------------------------------
    cout << "5.Create infer request..." << endl;
    auto infer_request = executable_network.CreateInferRequest();

    // --------------------------- 6. 准备输入数据 ----------------------------------------
    // 输入数据读入OpenCV Mat,由OpenCV完成数据输入模型前的Resize和Normalize
    cout << "6.Prepare Input..." << endl;
    cv::Mat img = cv::imread(imageFile);
    // 记录图片原始HW
    const size_t origin_width = (size_t)img.cols;
    const size_t origin_height = (size_t)img.rows;
    
    cv::Mat blob, frame;
    // Create a NCHW blob from a frame.
    const Size& inpSize = Size(640, 640);
    const Scalar& inpMean = Scalar(0.485 * 255, 0.456 * 255, 0.406 * 255);

    cv::Mat input_blob = cv::dnn::blobFromImage(img, 1.0, inpSize, Scalar(), true, false, CV_8U);
    cout << "original image's Size is:" << img.size << endl;
    cout << "input_blob's Size is:" << input_blob.size << endl;
    return 0;
}

blobFromImage运行结果

飞桨模型输入概要:

  1. input node's name and shape:
    a. image: [B, C, H, W],输入图片, RGB格式
    b. im_shape: [B, 2], 图片resize之后的模型尺寸,h, w
    c. scale_factor: [B, 2], 图片resize的缩放因子,scale_y, scale_x
  2. 图片均值mean一般是[0.485, 0.456, 0.406],方差std一般是[0.229, 0.224, 0.225],百度模型全用的是imagenet的mean和std
  3. is_scale表示在做图片归一化之前,图片的值先除以255.
  4. interp的方法默认是2,即cv2.INTER_LINEAR
参考百度工程师的回答

飞桨模型输出概要:飞桨模型输出名字在经过ONNX转化后经过前向的pass优化的,所以最终输出的名字对不上
飞桨的PPYOLOv2转为ONNX模型后模型输出名字对不上

参考链接:PaddleDetection模型导出教程

你可能感兴趣的:(C++中实现AI推理前的图像预处理_PPYOLOv2模型)