【OpenVINO】C#调用OpenVINO部署Al模型项目开发-4.创建OpenVINO 方法C++动态链接库-2

33 篇文章 18 订阅
17 篇文章 2 订阅

4.3 编写C++代码

(1)推理引擎结构体

  Core是OpenVINOTM 工具套件里的推理核心类,该类下包含多个方法,可用于创建推理中所使用的其他类。在此处,需要在各个方法中传递的仅仅是所使用的几个变量,因此选择构建一个推理引擎结构体,用于存放各个变量。

// @brief 推理核心结构体
typedef struct openvino_core {
    ov::Core core; // core对象
    std::shared_ptr<ov::Model> model_ptr; // 读取模型指针
    ov::CompiledModel compiled_model; // 模型加载到设备对象
    ov::InferRequest infer_request; // 推理请求对象
} CoreStruct;

  其中Core是OpenVINOTM 工具套件里的推理机核心,该模块只需要初始化; shared_ptr<ov::Model>是读取本地模型的方法,新版更新后,该方法发生了较大改动,可支持读取Paddlepaddle飞桨模型、onnx模型以及IR模型;CompiledModel指的是一个已编译的模型类,其主要是将读取的本地模型应用多个优化转换,然后映射到计算内核,由所指定的设备编译模型;nferRequest是一个推理请求类,在推理中主要用于对推理过程的操作。

(2)接口方法规划

  经典的OpenVINOTM 进行模型推理,一般需要八个步骤,主要是:初始化Core对象、读取本地推理模型、配置模型输入&输出、载入模型到执行硬件、创建推理请求、准备输入数据、执行推理计算以及处理推理计算结果。我们根据原有的八个步骤,对步骤进行重新整合,并根据推理步骤,调整方法接口。
  对于方法接口,主要设置为:推理初始化、配置输入数据形状、配置输入数据、模型推理、读取推理结果数据以及删除内存地址六个大类,其中配置输入数据形状要细分为配置图片数据形状以及普通数据形状,配置输入数据要细分为配置图片输入数据与配置普通数据输入,读取推理结果数据细分为读取float数据和int数据,因此,总共有6类方法接口,9个方法接口。

(3)初始化推理模型

  OpenVINOTM推理引擎结构体是联系各个方法的桥梁,后续所有操作都是在推理引擎结构体中的变量上操作的,为了实现数据在各个方法之间的传输,因此在创建推理引擎结构体时,采用的是创建结构体指针,并将创建的结构体地址作为函数返回值返回。推理初始化接口主要整合了原有推理的初始化Core对象、读取本地推理模型、载入模型到执行硬件和创建推理请求步骤,并将这些步骤所创建的变量放在推理引擎结构体中。
初始化推理模型接口方法为:

extern "C" __declspec(dllexport) void* __stdcall core_init(const wchar_t* model_file_wchar, const wchar_t* device_name_wchar);

 该方法返回值为CoreStruct结构体指针,其中model_file_wchar为推理模型本地地址字符串指针,device_name_wchar为模型运行设备名指针,在后面使用上述变量时,需要将其转换为string字符串,利用wchar_to_string()方法可以实现将其转换为字符串格式:

std::string model_file_path = wchar_to_string(model_file_wchar); 
std::string device_name = wchar_to_string(device_name_wchar);

  模型初始化功能主要包括:初始化推理引擎结构体和对结构体里面定义的其他变量进行赋值操作,其主要是利用InferEngineStruct中创建的Core类中的方法,对各个变量进行初始化操作:

CoreStruct* p = new CoreStruct(); // 创建推理引擎指针
p->model_ptr = p->core.read_model(model_file_path); // 读取推理模型
p->compiled_model = p->core.compile_model(p->model_ptr, "CPU"); // 将模型加载到设备
p->infer_request = p->compiled_model.create_infer_request(); // 创建推理请求

(4)配置输入数据形状
 在新版OpenVINOTM 2022.1 中,新增加了对Paddlepaddle模型以及onnx模型的支持,Paddlepaddle模型不支持指定指定默认bath通道数量,因此需要在模型使用时指定其输入;其次,对于onnx模型,也可以在转化时不指定固定形状,因此在配置输入数据前,需要配置输入节点数据形状。其方法接口为:

extern "C"  __declspec(dllexport) void* __stdcall set_input_image_sharp(void* core_ptr, const wchar_t* input_node_name_wchar, size_t * input_size);
extern "C"  __declspec(dllexport) void* __stdcall set_input_data_sharp(void* core_ptr, const wchar_t* input_node_name_wchar, size_t * input_size);

  由于需要配置图片数据输入形状与普通数据的输入形状,在此处设置了两个接口,分别设置两种不同输入的形状。该方法返回值是CoreStruct结构体指针,但该指针所对应的数据中已经包含了对输入形状的设置。第一个输入参数core_ptr是CoreStruct指针,在当前方法中,我们要读取该指针,并将其转换为CoreStruct类型:

CoreStruct* p = (CoreStruct*)core_ptr;

input_node_name_wchar 为待设置网络节点名,input_size 为形状数据数组,对图片数据,需要设置 [batch, dim, height, width] 四个维度大小,所以input_size数组传入4个数据,其设置在形状主要使用Tensor类下的set_shape()方法:

std::string input_node_name = wchar_to_string(input_node_name_wchar); // 将节点名转为string类型
ov::Tensor input_image_tensor = p->infer_request.get_tensor(input_node_name); // 读取指定节点Tensor
input_image_tensor.set_shape({ input_size[0],input_size[1],input_size[2],input_size[3] }); // 设置节点数据形状

(5)配置输入数据

 在新版OpenVINOTM 中,Tensor类的T* data()方法,其返回值为当前节点Tensor的数据内存地址,通过填充Tensor的数据内存,实现推理数据的输入。对于图片数据,其最终也是将其转为一维数据进行输入,不过为方便使用,此处提供了配置图片数据和普通数据的接口,对于输入为图片的方法接口:

extern "C"  __declspec(dllexport) void* __stdcall load_image_input_data(void* core_ptr, const wchar_t* input_node_name_wchar, uchar * image_data, size_t image_size);

  该方法返回值是CoreStruct结构体指针,但该指针所对应的数据中已经包含了加载的图片数据。第一个输入参数core_ptr是CoreStruct指针,在当前方法中,我们要读取该指针,并将其转换为CoreStruct类型;第二个输入参数input_node_name_wchar为待填充节点名,先将其转为string字符串:

std::string input_node_name = wchar_to_string(input_node_name_wchar);

  在该项目中,我们主要使用的是以图片作为模型输入的推理网络,模型主要的输入为图片的输入。其图片数据主要存储在矩阵image_data和矩阵长度image_size两个变量中。需要对图片数据进行整合处理,利用创建的data_to_mat () 方法,将图片数据读取到OpenCV中:

cv::Mat input_image = data_to_mat(image_data, image_size); 

  接下来就是配置网络图片数据输入,对于节点输入是图片数据的网络节点,其配置网络输入主要分为以下几步:
  首先,获取网络输入图片大小。
  使用InferRequest类中的get_tensor ()方法,获取指定网络节点的Tensor,其节点要求输入大小在Shape容器中,通过获取该容器,得到图片的长宽信息:

ov::Tensor input_image_tensor = p->infer_request.get_tensor(input_node_name);
int input_H = input_image_tensor.get_shape()[2]; //获得"image"节点的Height
int input_W = input_image_tensor.get_shape()[3]; //获得"image"节点的Width

  其次,按照输入要求,处理输入图片。
  在这一步,我们除了要按照输入大小对图片进行放缩之外,还要根据PaddlePaddle对模型输入的要求进行处理。因此处理图片其主要分为交换RGB通道、放缩图片以及对图片进行归一化处理。在此处我们借助OpenCV来实现。
  OpenCV读取图片数据并将其放在Mat类中,其读取的图片数据是BGR通道格式,PaddlePaddle要求输入格式为RGB通道格式,其通道转换主要靠一下方式实现:

cv::cvtColor(input_image, blob_image, cv::COLOR_BGR2RGB); 

接下来就是根据网络输入要求,对图片进行压缩处理:

cv::resize(blob_image, blob_image, cv::Size(input_H, input_W), 0, 0, cv::INTER_LINEAR);

  最后就是对图片进行归一化处理,其主要处理步骤就是减去图像数值均值,并除以方差。查询PaddlePaddle模型对图片的处理,其均值mean = [0.485, 0.456, 0.406],方差std = [0.229, 0.224, 0.225],利用OpenCV中现有函数,对数据进行归一化处理:

std::vector<float> mean_values{ 0.485 * 255, 0.456 * 255, 0.406 * 255 };
std::vector<float> std_values{ 0.229 * 255, 0.224 * 255, 0.225 * 255 };
std::vector<cv::Mat> rgb_channels(3);
cv::split(blob_image, rgb_channels); // 分离图片数据通道
for (auto i = 0; i < rgb_channels.size(); i++){
//分通道依此对每一个通道数据进行归一化处理
rgb_channels[i].convertTo(rgb_channels[i], CV_32FC1, 1.0 / std_values[i], (0.0 - mean_values[i]) / std_values[i]);
}
cv::merge(rgb_channels, blob_image); // 合并图片数据通道

  最后,将图片数据输入到模型中。
  在此处,我们重写了网络赋值方法,并将其封装到 fill_tensor_data_image(ov::Tensor& input_tensor, const cv::Mat& input_image)方法中,input_tensor为模型输入节点Tensor类,input_image为处理过的图片Mat数据。因此节点赋值只需要调用该方法即可:

fill_tensor_data_image(input_image_tensor, blob_image);

  对于普通数据的输入,其方法接口如下:

extern "C"  __declspec(dllexport) void* __stdcall load_input_data(void* core_ptr, const wchar_t* input_node_name_wchar, float* input_data);

  与配置图片数据不同点,在于输入数据只需要输入input_data数组即可。其数据处理哦在外部实现,只需要将处理后的数据填充到输入节点的数据内存中即可,通过调用自定义的fill_tensor_data_float(ov::Tensor& input_tensor, float* input_data, int data_size) 方法即可实现:

std::string input_node_name = wchar_to_string(input_node_name_wchar);
ov::Tensor input_image_tensor = p->infer_request.get_tensor(input_node_name); // 读取指定节点tensor
int input_size = input_image_tensor.get_shape()[1]; //获得输入节点的长度
fill_tensor_data_float(input_image_tensor,input_data, input_size); // 将数据填充到tensor数据内存上

(6)模型推理

  上一步中我们将推理内容的数据输入到了网络中,在这一步中,我们需要进行数据推理,这一步中我们留有一个推理接口:

extern "C"  __declspec(dllexport) void* __stdcall core_infer(void* core_ptr)

  进行模型推理,只需要调用CoreStruct结构体中的infer_request对象中的infer()方法即可:

CoreStruct* p = (CoreStruct*)core_ptr;
p->infer_request.infer();

(7)读取推理数据

  上一步我们对数据进行了推理,这一步就需要查询上一步推理的结果。对于我们所使用的模型输出,主要有float数据和int数据,对此,留有了两种数据的查询接口,其方法为:

extern "C"  __declspec(dllexport) void __stdcall read_infer_result_F32(void* core_ptr, const wchar_t* output_node_name_wchar, int data_size, float* infer_result);
extern "C"  __declspec(dllexport) void __stdcall read_infer_result_I32(void* core_ptr, const wchar_t* output_node_name_wchar, int data_size, int* infer_result);

  其中data_size为读取数据长度,infer_result 为输出数组指针。读取推理结果数据与加载推理数据方式相似,依旧是读取输出节点处数据内存的地址:

const ov::Tensor& output_tensor = p->infer_request.get_tensor(output_node_name);
const float* results = output_tensor.data<const float>();

  针对读取整形数据,其方法一样,只是在转换类型时,需要将其转换为整形数据即可。我们读取的初始数据为二进制数据,因此要根据指定类型转换,否则数据会出现错误。将数据读取出来后,将其放在数据结果指针中,并将所有结果赋值到输出数组中:

for (int i = 0; i < data_size; i++) {
*inference_result = results[i];
inference_result++;  
}

(8)删除推理核心结构体指针

  推理完成后,我们需要将在内存中创建的推理核心结构地址删除,防止造成内存泄露,影响电脑性能,其接口该方法为:

extern "C"  __declspec(dllexport) void __stdcall core_delet(void* core_ptr);

  在该方法中,我们只需要调用delete命令,将结构体指针删除即可。

4.4 编写模块定义文件

  我们在定义接口方法时,在原有方法的基础上,增加了extern "C" 、 __declspec(dllexport) 以及__stdcall 三个标识,其主要原因是为了让编译器识别我们的输出方法。其中,extern "C"是指示编译器这部分代码按C语言(而不是C++)的方式进行编译;__declspec(dllexport)用于声明导出函数、类、对象等供外面调用;__stdcall是一种函数调用约定。通过上面三个标识,我们在C++种所写的接口方法,会在dll文件中暴露出来,并且可以实现在C#中的调用。
  不过上面所说内容,我们在编辑器中可以通过模块定义文件(.def)所实现,在模块定义文件中,添加以下代码:

LIBRARY 
		"OpenVinoSharp"
EXPORTS
		core_init
		set_input_image_sharp
		set_input_data_sharp
		load_image_input_data
		load_input_data
		core_infer
		read_infer_result_F32
		read_infer_result_I32
		core_delet

  LIBRARY后所跟的为输出文件名,EXPORTS后所跟的为输出方法名。仅需要以上语句便可以替代extern "C" 、 __declspec(dllexport) 以及__stdcall的使用。

4.5 生成dll文件

  前面我们将项目配置输出设置为了生成dll文件,因此该项目不是可以执行的exe文件,只能生成不能运行。右键项目,选择重新生成/生成。在没有错误的情况下,会看到项目成功的提示。可以看到dll文件在解决方案同级目录下\x64\Release\文件夹下。
使用dll文件查看器打开dll文件,如图1- 11所示;可以看到,我们创建的四个方法接口已经暴露在dll文件中。
dll文件方法输出目录

图1- 11 dll文件方法输出目录

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: C语言中创建动态链接库供C程序使用的步骤如下: 1. 写好要封装的C语言函数或者模块的代码,并将其保存为一个或多个C文件。 2. 编写一个头文件,将要使用的函数或者模块的声明放在该头文件中。 3. 打开终端或者命令提示符,使用GCC编译器将C文件编译为目标文件。命令如下: `gcc -c 文件名.c -o 输出的目标文件名.o` 这样会将C文件编译为目标文件,目标文件是二进制文件,用来存储编译后的机器码。 4. 将编译好的目标文件打包成动态链接库动态链接库的命名规则为"libxxx.so",其中xxx为动态链接库的名字。命令如下: `gcc -shared 目标文件1.o 目标文件2.o -o libxxx.so` 这样会将目标文件打包成动态链接库动态链接库是一个二进制文件,包含了函数或者模块的机器码。 5. 将生成动态链接库文件放置到C程序的运行路径下,使用时可通过指定库的名字链接到该库。 6. 在C程序中引入头文件,并调用动态链接库中的函数或者模块。 以上是使用GCC编译器创建动态链接库供C程序使用的基本步骤。通过这种方式封装功能,可以实现代码的模块化和重用,提高程序的可维护性和可扩展性。 ### 回答2: 使用者使用动态链接库的主要目的是为了方便地重复使用某些功能代码。在C语言中,可以使用如下步骤来创建并使用动态链接库。 首先,创建动态链接库的源代码文件,该文件包含了要提供的功能代码。可以使用C语言编写这个文件,其中可能包含一些函数和全局变量等。 接下来,使用编译器将源代码文件编译成目标文件。在这个过程中,需要使用适当的编译选项来指定生成动态链接库而不是可执行文件。比如,在GCC编译器中,可以使用"-shared"选项来生成动态链接库。 然后,使用编译器将目标文件链接成动态链接库文件。这一步会生成扩展名为".so"(在Linux系统上)或".dll"(在Windows系统上)的文件。在该步骤中,需要提供一些额外的链接选项,以确保正确地生成动态链接库。 最后,使用者可以在自己的C语言程序中引用和使用动态链接库。可以通过在程序中包含相应的头文件并使用相关的函数和变量来调用动态链接库中的功能。 需要注意的是,使用者在编译和链接自己的程序时,需要指定动态链接库的位置和名称,以便在运行时正确加载和使用动态链接库。这可以通过编译选项和链接选项来实现。 总之,通过以上步骤,可以创建一个供C语言程序使用的动态链接库,并在程序中使用其中的功能代码。这样可以提高代码的复用性和可维护性,同时也便于程序的调试和更新。 ### 回答3: 编程语言C中的动态链接库(Dynamic Link Library,简称DLL)是一个可重用的代码和数据集合,可以在不同的程序中被调用。以下是创建动态链接库供C使用的基本步骤: 1. 编写C代码:首先,编写包含所需功能的C代码。将这些代码组织成一个或多个函数,这些函数可以是库的接口。 2. 创建头文件:创建一个头文件(.h文件),其中包含库的函数声明和必要的常量和类型定义。这个头文件将作为客户端程序与动态链接库之间的接口。 3. 编译动态链接库:使用C编译器(例如gcc)将C代码编译成目标文件,使用以下命令生成位置无关的目标文件: ``` gcc -c -fPIC library.c -o library.o ``` 其中,`-c`选项表示只编译不链接,`-fPIC`选项表示生成位置无关的代码,`library.c`是你的源代码文件名,`library.o`是生成的目标文件名。 4. 创建动态链接库:使用以下命令将目标文件创建动态链接库: ``` gcc -shared -o liblibrary.so library.o ``` 其中,`-shared`选项表示生成动态链接库,`-o liblibrary.so`指定输出的库文件名为`liblibrary.so`。 5. 安装动态链接库:将生成动态链接库文件(`liblibrary.so`)复制到系统的默认库目录(例如`/usr/lib`)。使用以下命令: ``` sudo cp liblibrary.so /usr/lib ``` 注意:根据操作系统和环境设置,可能需要提供管理员权限。 6. 使用动态链接库:在你的C程序中,通过包含头文件(步骤2)并使用函数声明来调用动态链接库中的函数。编译时需要链接动态链接库,可以使用以下命令: ``` gcc client.c -o client -llibrary ``` 其中,`client.c`是你的客户端程序源代码文件名,`-llibrary`表示链接名为`liblibrary.so`的动态链接库。 这样,你就成功地创建了一个动态链接库,供其他C程序调用。在客户端程序中,只需要包含头文件并链接动态链接库,就能使用其中定义的函数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

椒颜皮皮虾྅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值