Paddle模型部署之利用C++创建Dll文件以供C#调用

本文介绍了如何使用C++创建DLL,集成Paddle模型进行图像推理,并在C#环境中调用该DLL。关键步骤包括加载模型、定义导出函数、设置静态变量以优化性能,以及在C#端进行图像预处理和结果后处理。文章还涉及了Halcon库在图像处理中的应用。
摘要由CSDN通过智能技术生成

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对象

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值