1. C++侧生成Dll
1.1 添加所有需要的头文件
#include "pch.h"
#include <chrono>
#include <iostream>
#include <fstream>
#include <algorithm>
#include <iomanip>
#include <memory>
#include <numeric>
#include <vector>
#include <gflags/gflags.h>
#include "paddle/include/paddle_inference_api.h"
//halcon处理相关操作
#include "CHalconTool.h"
#define uchar unsigned char
using namespace std;
using paddle_infer::Config;
using paddle_infer::Predictor;
using paddle_infer::CreatePredictor;
1.2 EXPORT推理函数
定义export关键字并声明需要供C#调用的推理函数
//第一步,定义宏
#ifdef EXPORT_DLL
#else
#define EXPORT_DLL extern "C" _declspec(dllexport) //指的是允许将其给外部调用
#endif
//第二步,相当于声明sum这个函数,作为一个导出方法,外部可见
EXPORT_DLL void Infer(uchar image_data[], uchar* output);
1.3 定义模型为静态变量
将模型加载定义为静态变量,该步骤的目的是使得后续调用Dll时模型只加载一次即可,而推理过程可以做成函数以供反复使用
std::shared_ptr<Predictor> InitPredictor()
{
Config config;
//神经元网络的模型
config.SetModel("pplite/model.pdmodel", "pplite/model.pdiparams");
if (FLAGS_use_ort)
{
// 使用onnxruntime推理
config.EnableONNXRuntime();
// 开启onnxruntime优化
config.EnableORTOptimization();
}
else
{
config.EnableMKLDNN();
}
// Open the memory optim.
config.EnableMemoryOptim();
return CreatePredictor(config);
}
// 模型加载做成静态变量,以后模型就只加载一次,推理过程进行多次循环调用即可
static auto predictor = InitPredictor();
static const int INPUT_H = 128;
static const int INPUT_W = 512;
InitPredictor函数需放置于调用之前,利用FLAGS_use_ort变量来控制是否使用onnxruntime进行推理加速。(实测开启onnx在CPU推理下可以加速一倍)
将模型predictor设置为静态变量,Dll被调用时便执行初始化,后续不会释放可以反复使用
可以将一些在推理函数中会反复利用到的量也设置为静态变量。(如INPUT_W/H)
1.4 推理
接收从C#端传过来的图片数据执行推理并写入output数组中变相地返回推理结果
void Infer(uchar image_data[], uchar* output)
{
CHalconTool* halconTool = new CHalconTool();
std::vector<float> input_data;
input_data.resize(3 * INPUT_W * INPUT_H);
for (int i = 0; i < 196608; ++i)
{
input_data.at(i) = image_data[i];
}
//--------特别注意,此处,先height,后width
std::vector<int> input_shape = { 1, 3, INPUT_H, INPUT_W };//1是batchsize
std::vector<float> out_data;
//模型推理
run(predictor.get(), input_data, input_shape, &out_data);
//对模型推理结果做Softmax操作,以确认每个像素点各自属于的类别
for (int i = 0; i < INPUT_H * INPUT_W; i++)
{
float tmp = 0;
int idx = 0;
for (int classIndex = 0; classIndex < 4; classIndex++)
{
float val = out_data.at(INPUT_H * INPUT_W * classIndex + i);
if (val > tmp)
{
tmp = val;
idx = classIndex;
}
}
output[i] = idx * 50;
}
}
C#端读取图片并将数据按顺序写入数组image_data中,并建立空的数组output以接收推理结果,参数传递时image_data传递的是数据,而output传递的是数组的首元素地址
定义模型输入向量input_data以接收传入的图像数据,定义模型输出向量output_data以存储模型推理结果
定义模型输入的尺寸input_shape,该向量决定了模型输入的维度,若是单张图片进行推理,batch维度设置为1,多张一起推理则相应地设置为图片数量,此时传入图片的数据则需要按序放入input_data向量中
此处使用的模型为pp_lite,输出结果为3维张量{11, 128, 512},其中11为对每个像素点推理的类别数,128和512为输出的尺寸,此处自行实现了Softmax函数,处理结果为{128, 512}的二维矩阵,每个像素点为对应概率最大的类的下标。(此处将下标乘上50倍以方便后续观察结果)
1.5 推理过程
此处对C++部署Paddle模型的推理过程进行一下梳理和解释
void run(Predictor* predictor, const std::vector<float>& input,
const std::vector<int>& input_shape, std::vector<float>* out_data)
{
int input_num = std::accumulate(input_shape.begin(), input_shape.end(), 1,
std::multiplies<int>());
// 获取输入 Tensor
auto input_names = predictor->GetInputNames();
auto output_names = predictor->GetOutputNames();
auto input_t = predictor->GetInputHandle(input_names[0]);
input_t->Reshape(input_shape);
input_t->CopyFromCpu(input.data());
predictor->Run();
auto output_t = predictor->GetOutputHandle(output_names[0]);
std::vector<int> output_shape = output_t->shape();
int out_num = std::accumulate(output_shape.begin(), output_shape.end(), 1, std::multiplies<int>());
out_data->resize(out_num);
output_t->CopyToCpu(out_data->data());
}
run函数定义了推理过程,predictor为模型,需要接收输入数据,输入数据的维度信息,输出数据的空向量作为参数。
定义输入张量input_t和其维度,将输入图像的数据写入其中;执行推理得到输出张量数据output_t,将输出张量按序写入向量中以供后续执行其他操作和返回至Dll调用端
2. C#侧调用Dll
C# 调用Dll以加载paddle预训练模型执行推理
2.1 引入所需指令或程序集
using System;
using System.IO;
using System.Runtime.InteropServices;
using HalconDotNet;
using System.Diagnostics;
该处HalconDotNet是为了用于处理输入图片,如图片读取,RGB数据提取,图像resize等操作
2.2 声明调用Dll以及函数
[DllImport("Dll1.dll", CallingConvention = CallingConvention.Cdecl)]
public extern static void Infer(byte[] input_data, ref byte output);
关键步骤,声明所要调用的Dll名称以及所需调用的函数,此处传入参数为输入数据以及输出数据接收数组的首地址指针,该处为固定写法,C#的byte类型对应着C++的unsigned char类型,参数1为输入数据,即待推理的图像数据;参数2为用来接收推理结果的数组,该处利用ref关键字表示传入指针,数据类型为byte,而output是一维数组
2.2 图片读取和预处理
string imgpath = Path.Combine("D:/testdll/ConsoleApp1/bin/x64/Release/ori_images", image_name);
string output_image_path = "D:/testdll/ConsoleApp1/bin/x64/Release/output_images";
HObject ho_Image;
HTuple ori_Width = new HTuple(), ori_Height = new HTuple();
HTuple para = new HTuple("constant");
//初始化本地并输出图形变量
HOperatorSet.GenEmptyObj(out ho_Image);
//初始化ho_Image变量,并读取图像文件,read_image
ho_Image.Dispose();
HOperatorSet.ReadImage(out ho_Image, imgpath);
HOperatorSet.MirrorImage(ho_Image, out ho_Image, "column");
//初始化图像宽、高变量并获取图像的宽和高
ori_Width.Dispose(); ori_Height.Dispose();
HObject hoZoomImage;
HOperatorSet.GenEmptyObj(out hoZoomImage);
hoZoomImage.Dispose();
HOperatorSet.GetImageSize(ho_Image, out ori_Width, out ori_Height);
HOperatorSet.ZoomImageSize(ho_Image, out hoZoomImage, 512, 128, para);
利用halcon读取图片并进行一些所需的预处理,预处理操作根据个人需要而定,此处执行了图像的按纵轴左右翻转(HOperatorSet.MirrorImage);获取到原图像尺寸(HOperatorSet.GetImageSize);变换输出图像尺寸,插值法resize(HOperatorSet.ZoomImageSize)。
2.3 将读取图像数据写入数组以供模型推理
byte[] input_data = new byte[3 * 128 * 512];
FromHObjectToVector(hoZoomImage, input_data);
定义输入数据一维数组,数组大小为输入图像的维度乘积;将HObject类型的RGB图像数据按顺序写入数组input_data中,原因是paddle 模型部署后输入数据是按维度顺序放置于一维向量当中,FromHObjectToVector函数为自行实现,代码如下
public void FromHObjectToVector(HObject image, byte[] input_data)
{
HTuple channels, width, height, type;
HOperatorSet.ConvertImageType(image, out image, "byte");
HOperatorSet.CountChannels(image, out channels);
HTuple pointerR = new HTuple();
HTuple pointerG = new HTuple();
HTuple pointerB = new HTuple();
HOperatorSet.GetImagePointer3(image, out pointerR, out pointerG, out pointerB, out type, out width, out height);
//Console.WriteLine($"-----------here width={width},height={height}");
byte[] R = new byte[width* height];
byte[] G = new byte[width * height];
byte[] B = new byte[width * height];
Marshal.Copy(pointerR, R, 0, width * height);
Marshal.Copy(pointerG, G, 0, width * height);
Marshal.Copy(pointerB, B, 0, width * height);
for (int i = 0; i < height; ++i)
{
for (int j = 0; j < width; ++j)
{
input_data[i * width + j + 0] = R[i * width + j];
input_data[i * width + j + width * height] = G[i * width + j];
input_data[i * width + j + width * height * 2] = B[i * width + j];
}
}
}
2.4 调用Dll中的推理函数执行推理
Infer(input_data, ref output[0]);
关键步骤!此处调用Dll中的推理函数,传入两个参数,参数1为纯数据传入,参数2为数组指针,尤其是参数2的写法需要注意,利用ref关键字并指向output数组的第一个元素。
2.5 对推理结果进行后处理
ho_Image.Dispose();
hoZoomImage.Dispose();
// 第五个参数需要传递数组的第一个元素指针
IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(output, 0);
HOperatorSet.GenImage1(out ho_Image, "byte", 512, 128, ptr);
HOperatorSet.ZoomImageSize(ho_Image, out hoZoomImage, ori_Width, ori_Height, "nearest_neighbor");
//把分割结果保存下来,文件名为SegmentOut_1.bmp
HOperatorSet.MirrorImage(hoZoomImage, out hoZoomImage, "column");
//HOperatorSet.WriteImage(hoZoomImage, "bmp", 0, Path.Combine(output_image_path, image_name));
//释放变量
ho_Image.Dispose();
hoZoomImage.Dispose();
ori_Width.Dispose();
ori_Height.Dispose();
先初始化两个HObject数据ho_Image,hoZoomImage,以接收后续处理出来的数据
利用halcon中的HOperatorSet.GenImage1算子将ho_Image创建为一个定义尺寸的张量,并把output数组中的数据进行写入。
此处有一需要注意的点:GenImage1最后一个参数需要一个HTuple类型的指针,而我们是要将output数组中的数据写入ho_Image中,该处利用
IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(output, 0);
获取到output第一个元素的指针。
接着将数据resize至原图像尺寸,该处使用最近邻插值,以保证图像边缘不会出现其他类别。
将数据进行翻转
最后释放到HObject对象