OpenVINOSharp常用API详解与演示

openlab_4276841a 更新于 7月前

文章作者:颜国进  英特尔边缘计算创新大使

技术指导:武卓,李翊玮

OpenVINO™ 工具套件可以加快深度学习视觉应用开发速度,帮助用户在从边缘到云的各种英特尔平台上,更加方便快捷的将 AI 模型部署到生产系统中。

C# 是由 C 和 C++ 衍生出来的一种安全的、稳定的、简单的、优雅的面向对象编程语言,它综合了 VB 简单的可视化操作和 C++ 的高运行效率,成为支持成为.NET 开发的首选语言。作为人工智能开发人员,如果你希望在 C# 端使用 OpenVINO™ ,OpenVINOSharp 将是你的首选,并且制作了 NuGet 程序包,实现在 C# 端了一站式安装与使用 OpenVINO™

OpenVINOSharp GitHub: https://github.com/guojin-yan/OpenVinoSharp

OpenVINOSharp 在制作时参考了OpenVINO™ C++ API,因此对于之前使用过OpenVINO™ 的人十分友好。下面表格向我们展示了 C# 与 C++ API的对应关系

Class

C++ API

C# API

说明

Core class

ov::Core

Core

OpenVINO运行时核心实体类

Model class

ov::Model

Model

用户自定义模型类

CompiledModel class

ov::CompiledModel

CompiledModel

已编译的模型类

Output class

ov:: Output<ov::Node>

Output

节点输出的句柄类

Input class

ov:: Input <ov::Node>

Input

节点输入的句柄类

InferRequest class

ov::InferRequest

ov::InferRequest

以异步或同步方式运行推断请求的类

Tensor class

ov::Tensor

Tensor

张量

Shape class

ov::Shape

Shape

张量的形状类

在本文中,将会根据模型部署的一般步骤,演示从模型加载到推理的方法函数使用方式,并于C++ API 做对比。

1.1 安装OpenVINOSharp

OpenVINOSharp 支持 NuGet 程序包安装方式,这与在C++中安装过程相比,较为简单,并且程序包中包含了 OpenVINO™ 2023.0 发行版本,可以通过 NuGet 安装后直接使用。

如果使用Visual Studio 编译该项目,则可以通过 NuGet 程序包管理功能直接安装即可:

如果通过dotnet命令方式安装,通过下面语句进行安装即可:

dotnet add package OpenVinoSharp.win

1.2 导入程序集

OpenVINOSharp 程序集全部在命名空间 OpenVinoSharp 下,因此若要使用 OpenVINOSharp ,需要先引入命名空间:

using OpenVinoSharp;

1.3 初始化OpenVINO 运行时内核

Core类代表一个OpenVINO运行时核心实体,后续的读取模型、加载模型等方法都需要通过 Core 类进行创建,在封装C# API 时,为了与 C++ API 对应,也对 Core 类进行了封装,并封装了与 C++ API 中对应的方法

在C#中的初始化方式:

Core core = new Core();

在C++中的初始化方式:

ov::Core core;

1.4 加载并获取模型信息

1.4.1 加载模型

OpenVINO™ 2022.1版本更新之后,加载,下面是所使用的 API 方法:

API

作用

Core.read_model ()

将模型从硬盘载入内存,并返回Model对象。

在C#中加载模型的方式:

Model model = core.read_model(model_path);

在C++中的初始化方式:

std::shared_ptr model = core.read_model(model_path);

1.4.2 获取模型信息

通过 Core.read_model ()方法获得的 Model 对象和通过 Core.compile_model ()方法获得的 CompiledModel 对象,都支持直接访问属性获取输入与输出层信息。以Model对象获取模型信息为例,下面是所使用的 API 方法:

API

作用

Model.get_friendly_name()

获取模型的friendly name。

Model.input()

获取模型的输入层,并返回 Input对象。

Model.output()

获取模型的输出层,并返回 Output对象。

Input/Output 主要是封装了模型网络层,可以通过下面 API 实现获取模型的详细信息:

API

作用

Output.get_any_name()

获取模型网络层的名字。

Output.get_element_type()

获取模型网络层的数据类型,并返回 OvType对象,OvType主要封装了网络的基本数据类型。

Output.get_shape()

获取模型网络层的形状,并返回 Shape对象,Shape封装了网络层的形状数组。

在 C# 中通过下方代码,可以直接获取模型的输入、输入层以及模型的friendly name:

string model_name = model.get_friendly_name();

Input input = model.input();

Output output = model.output();

然后将模型具体信息打印到控制台页面:

Console.WriteLine("Model name: {0}", model_name);

Console.WriteLine("/------- [In] -------/");

Console.WriteLine("Input name: {0}", input.get_any_name());

Console.WriteLine("Input type: {0}", input.get_element_type().to_string());

Console.WriteLine("Input shape: {0}", input.get_shape().to_string());

Console.WriteLine("/------- [Out] -------/");

Console.WriteLine("Output name: {0}", output.get_any_name());

Console.WriteLine("Output type: {0}", output.get_element_type().to_string());

Console.WriteLine("Output shape: {0}", output.get_shape().to_string());

获取模型网络层信息如下:

Model name: torch_jit

/------- [In] -------/

Input name: data

Input type: float

Input shape: [1,3,224,224]

/------- [Out] -------/

Output name: prob

Output type: float

Output shape: [1,1000]

同样的输出信息,我们使用 C++ API 实现如下:

std::cout << "Model name: " << model->get_friendly_name() << std::endl;

ov::Output<ov::Node> input = model->input();

std::cout << "/------- [In] -------/" << std::endl;

std::cout << "Input name: " << input.get_any_name() << std::endl;

std::cout << "Input type: " << input.get_element_type().c_type_string() << std::endl;

std::cout << "Input shape: " << input.get_shape().to_string() << std::endl;

ov::Output<ov::Node> output = model->output();

std::cout << "/------- [Out] -------/" << std::endl;

std::cout << "Output name: " << output.get_any_name() << std::endl;

std::cout << "Output type: " << output.get_element_type().c_type_string() << std::endl;

std::cout << "Output shape: " << output.get_shape().to_string() << std::endl;

1.5 编译模型并创建推理请求

在读取本地模型后,调用模型编译方法将模型编译为可以在目标设备上执行的 compile_model 对象,并通过该对象创建用于推断已编译模型的推断请求对象。下面是所使用的 API 方法:

API

作用

Core.compile_model()

将模型编译为可以在目标设备上执行的 compile_model 对象。

CompiledModel.create_infer_request()

创建用于推断已编译模型的推断请求对象,创建的请求已经分配了输入和输出张量。

在 C# 中编译模型并创建推理请求的方式:

CompiledModel compiled_model = core.compile_model(model, "AUTO");

InferRequest infer_request = compiled_model.create_infer_request();

使用C++ API中编译模型并创建推理请求的方式:

CompiledModel compiled_model = core.compile_model(model, "AUTO");

InferRequest infer_request = compiled_model.create_infer_request();

1.6 张量Tensor

1.6.1 张量的获取与设置

在创建推理请求后,系统会自动创建和分配输入和输出的张量,张量可以通过InferRequest 对象获得,并且可以自定义张量并加载到模型指定节点;可以根据张量的输入输出序号、名称以及模型节点Node对象获取和设置,主要C# API 如下:

API

作用

InferRequest.set_tensor()

设置要推断的输入/输出张量。

InferRequest.set_input_tensor()

设置要推断的输入张量。

InferRequest.set_output_tensor()

设置要推断的输出张量

InferRequest.get_tensor()

获取用于推理的输入/输出张量。

InferRequest.get_input_tensor()

获取用于推理的输入张量。

InferRequest.get_output_tensor()

获取用于推理的输出张量。

1.6.2 张量的信息获取与设置

张量中主要包含的信息有张量的形状(Shape)、张量的数据格式(OvType-> element.Type)以及张量中的内存数据。可以通过以下API方法操作张量的参数:

API

作用

Tensor.set_shape ()

给张量设置一个新的形状。

Tensor.get_shape()

获取张量的形状。

Tensor.get_element_type()

获取张量的数据类型。

Tensor.get_size()

获取张量的数据长度。

Tensor.get_byte_size()

获取张量的字节大小。

Tensor.data()

获取张量的内存地址。

Tensor.set_data<T>()

将指定类型的数据加载到张量内存下。

Tensor.get_data<T>()

从张量中读取指定类型的数据。

以上方法是对张量的一些基础操作,除了set_data、get_data是OpenVINOSharp独有的,其他接口都与C++API一致。

1.7 加载推理数据

1.7.1 获取输入张量

对于单输入的模型可以直接通过get_input_tensor()方法获得,并调用Tensor的相关方法获取Tensor的相关信息,C# 代码如下所示:

Tensor input_tensor = infer_request.get_input_tensor();

Console.WriteLine("/------- [Input tensor] -------/");

Console.WriteLine("Input tensor type: {0}", input_tensor.get_element_type().to_string());

Console.WriteLine("Input tensor shape: {0}", input_tensor.get_shape().to_string());

Console.WriteLine("Input tensor size: {0}", input_tensor.get_size());

获取输出结果为:

/------- [Input tensor] -------/

Input tensor type: f32

Input tensor shape: Shape : {1, 3, 224, 224}

Input tensor size: 150528

对于上述的同样输出内容,我们也可以通过C++ API 实现,C++ 代码如下:

ov::Tensor input_tensor = infer_request.get_input_tensor();

std::cout << "/------- [Input tensor] -------/" << std::endl;

std::cout << "Input tensor type: " << input_tensor.get_element_type().c_type_string() << std::endl;

std::cout << "Input tensor shape: " << input_tensor.get_shape().to_string() << std::endl;

std::cout << "Input tensor size: " << input_tensor.get_size() << std::endl;

1.7.2 添加推理数据

这一步主要是将处理好的图片数据加载到Tensor数据内存中,OpenVINO的API中提供了访问内存地址的接口,可以获取数据内存首地址,不过为了更好的加载推理数据,我们此处封装了set_data<T>()方法,可以实现将处理后的图片数据加载到数据内存上。在C#中的代码为:

Mat input_mat = new Mat();

Shape input_shape = input_tensor.get_shape();

long channels = input_shape[1];

long height = input_shape[2];

long width = input_shape[3];

float[] input_data = new float[channels * height * width];

Marshal.Copy(input_mat.Ptr(0), input_data, 0, input_data.Length);

input_tensor.set_data(input_data);

下面是在C++中实现上述功能的代码:

cv::Mat input_mat;

float* input_data = input_tensor.data<float>();

ov::Shape input_shape = input_tensor.get_shape();

size_t channels = input_shape[1];

size_t height = input_shape[2];

size_t width = input_shape[3];

for (size_t c = 0; c < channels; ++c) {

    for (size_t h = 0; h < height; ++h) {

        for (size_t w = 0; w < width; ++w) {

            input_data[c * height * width + h * width + w] = input_mat.at<cv::Vec<float, 3>>(h, w)[c];

        }

    }

}

1.8 模型推理

在加载完推理数据后,就可以调用模型推理的API方法推理当前数据,主要使用到的API方法为:

API

作用

InferRequest.infer()

在同步模式下推断指定的输入。

调用该方法也较为简单,只需要调用该API接口即可,在C#中的代码为:

infer_request.infer();

C++中的代码与C++中一致。

1.9 获取推理结果

对于单输出的模型可以直接通过get_output_tensor()方法获得,并调用Tensor的相关方法获取Tensor的相关信息,C# 代码如下所示:

Tensor output_tensor = infer_request.get_output_tensor();

Console.WriteLine("/------- [Output tensor] -------/");

Console.WriteLine("Output tensor type: {0}", output_tensor.get_element_type().to_string());

Console.WriteLine("Output tensor shape: {0}", output_tensor.get_shape().to_string());

Console.WriteLine("Output tensor size: {0}", output_tensor.get_size());

获取输出output_tensor信息为:

/------- [Output tensor] -------/

Output tensor type: f32

Output tensor shape: Shape : {1, 1000}

Output tensor size: 1000

对于输出Tensor,我们只需要读取输出内存上的数据即可,此处我们封装了get_data<T>()方法,可以直接获取输出内存上的数据,在C#中的代码为:

float[] result = output_tensor.get_data(1000);

同样获取推理结果,在C++中的代码为:

const float* output_data = output_tensor.data<const float>();

float result[1000];

for (int i = 0; i < 1000; ++i) {

result[i] = *output_data;

output_data++;

}

在获取结果后,后续的处理需要根据模型的输出类型做相应的处理。

1.10 释放分配的内存

由于C#在封装时采用的C API 接口实现的,因此在C#中会产生较多的 非托管内存,若该对象出现循环重复创建,会导致过多的内存未释放导致内存泄漏,因此对于临时创建的对象在使用后要即使销毁,销毁方式也较为简单,只需要调用对象的dispose()方法即可。

output_tensor.dispose();

input_shape.dispose();

infer_request.dispose();

compiled_model.dispose();

input.dispose();

output.dispose();

model.dispose();

core.dispose();

1.11 Yolov8分类模型示例

下面代码展示了Yolov8分类模型使用OpenVINOSharp API方法部署模型的完整代码:

using OpenCvSharp;

using OpenCvSharp.Dnn;

using OpenVinoSharp;

using System.Data;

using System.Runtime.InteropServices;

namespace test_openvinosharp_api

{

    internal class Program

    {

        static void Main(string[] args)

        {

            string model_path = "E:\\GitSpace\\OpenVinoSharp\\model\\yolov8\\yolov8s-cls.xml";

            Core core = new Core(); // 初始化推理核心

            Model model = core.read_model(model_path); // 读取本地模型

            CompiledModel compiled_model = core.compile_model(model, "AUTO"); // 便哟模型到指定设备

            // 获取模型的输入输出信息

            Console.WriteLine("Model name: {0}", model.get_friendly_name());

            Input input = compiled_model.input();

            Console.WriteLine("/------- [In] -------/");

            Console.WriteLine("Input name: {0}", input.get_any_name());

            Console.WriteLine("Input type: {0}", input.get_element_type().to_string());

            Console.WriteLine("Input shape: {0}", input.get_shape().to_string());

            Output output = compiled_model.output();

            Console.WriteLine("/------- [Out] -------/");

            Console.WriteLine("Output name: {0}", output.get_any_name());

            Console.WriteLine("Output type: {0}", output.get_element_type().to_string());

            Console.WriteLine("Output shape: {0}", output.get_shape().to_string());

            // 创建推理请求

            InferRequest infer_request = compiled_model.create_infer_request();

            // 获取输入张量

            Tensor input_tensor = infer_request.get_input_tensor();

            Console.WriteLine("/------- [Input tensor] -------/");

            Console.WriteLine("Input tensor type: {0}", input_tensor.get_element_type().to_string());

            Console.WriteLine("Input tensor shape: {0}", input_tensor.get_shape().to_string());

            Console.WriteLine("Input tensor size: {0}", input_tensor.get_size());

            // 读取并处理输入数据

            Mat image = Cv2.ImRead(@"E:\GitSpace\OpenVinoSharp\dataset\image\demo_7.jpg");

            Mat input_mat = new Mat();

            input_mat = CvDnn.BlobFromImage(image, 1.0 / 255.0, new Size(224, 224), 0, true, false);

            // 加载推理数据

            Shape input_shape = input_tensor.get_shape();

            long channels = input_shape[1];

            long height = input_shape[2];

            long width = input_shape[3];

            float[] input_data = new float[channels * height * width];

            Marshal.Copy(input_mat.Ptr(0), input_data, 0, input_data.Length);

            input_tensor.set_data(input_data);

            // 模型推理

            infer_request.infer();

            // 获取输出张量

            Tensor output_tensor = infer_request.get_output_tensor();

            Console.WriteLine("/------- [Output tensor] -------/");

            Console.WriteLine("Output tensor type: {0}", output_tensor.get_element_type().to_string());

            Console.WriteLine("Output tensor shape: {0}", output_tensor.get_shape().to_string());

            Console.WriteLine("Output tensor size: {0}", output_tensor.get_size());

            // 获取输出数据

            float[] result = output_tensor.get_data<float>(1000);

            List<float[]> new_list = new List<float[]> { };

            for (int i = 0; i < result.Length; i++)

            {

                new_list.Add(new float[] { (float)result[i], i });

            }

            new_list.Sort((a, b) => b[0].CompareTo(a[0]));

            KeyValuePair<int, float>[] cls = new KeyValuePair<int, float>[10];

            for (int i = 0; i < 10; ++i)

            {

                cls[i] = new KeyValuePair<int, float>((int)new_list[i][1], new_list[i][0]);

            }

            Console.WriteLine("\n Classification Top 10 result : \n");

            Console.WriteLine("classid probability");

            Console.WriteLine("------- -----------");

            for (int i = 0; i < 10; ++i)

            {

                Console.WriteLine("{0}     {1}", cls[i].Key.ToString("0"), cls[i].Value.ToString("0.000000"));

            }

            // 销毁非托管内存

            output_tensor.dispose();

            input_shape.dispose();

            infer_request.dispose();

            compiled_model.dispose();

            input.dispose();

            output.dispose();

            model.dispose();

            core.dispose();

        }

    }

}

1.12 总结

在本文中我们基于模型推理流程,演示了OpenVINOSharp API使用方法,并和OpenVINO C++API进行了对比,展示了OpenVINOSharp API与C++API在使用的区别,这也对使用过C++ API的开发者十分友好,上手会十分容易。

在本文中我们只展示了基础的模型推理流程代码,也对各个API进行了测试,针对其他比较高级的API方法,我们后续会继续进行测试其他API方法,向各位开发者展示其用法。

总的来说,目前OpenVINOSharp已经完全支持在Windows环境下的安装使用,欢迎各位开发者安装使用,如有相关问题或优化方法,也欢迎大家提出意见与指导。

0个评论