C++的API跑神经网络模型相比于Python可以得到数倍的提升
同时类似TensorRT会提供非常多的模型优化,比如int8推理
而且实际项目中一般考虑性能等各种因素也不会直接使用Python
而是会选择C++或者Java等
这次我在做项目中遇到了必须要用C++来做模型部署的问题,这里做个记录
我这次选择了ONNXRuntime这个框架做CPU推理
ONNXRuntime的GPU推理和TensorRT将在未来有空的时候进行跟进
另外本来我是想直接用Tensorflow的C++api,但是坑爹的是windows10版的源码缺头文件,无法通过编译,所以就搁置了
ONNXRuntime
onnx作为一个非常优秀的跨平台的深度学习工具,其他框架训练的模型均可在上进行使用部署,作为cpu平台部署的利器,因为不像gpu平台,英伟达提供了tensorrt进行加速部署.这里是在win10借助vs2019上面进行安装编译这个平台
安装ONNXRuntime
(1)去下载安装包https://www.nuget.org/
(2)将安装包拷贝到win上的磁盘上,指定一个目录存放
(3)打开vs2019,工具->NuGet程序包管理器->程序包管理控制平台
这是一个命令行平台
接下来安装即可
PM> Install-Package Microsoft.ML.OnnxRuntime -Source C:\Users\pc\Desktop\Cache
正在尝试收集与目标为“native,Version=v0.0”的项目“onnxDemo”有关的包“Microsoft.ML.OnnxRuntime.1.6.0”的依赖项信息
收集依赖项信息花费时间 20 ms
正在尝试解析程序包“Microsoft.ML.OnnxRuntime.1.6.0”的依赖项,DependencyBehavior 为“Lowest”
解析依赖项信息花费时间 0 ms
正在解析操作以安装程序包“Microsoft.ML.OnnxRuntime.1.6.0”
已解析操作以安装程序包“Microsoft.ML.OnnxRuntime.1.6.0”
从“C:\Users\Administrator\AppData\Local\NuGet\Cache”检索包“Microsoft.ML.OnnxRuntime 1.6.0”
正在将程序包“Microsoft.ML.OnnxRuntime.1.6.0”添加到文件夹“C:\Users\Administrator\source\repos\onnxDemo\packages”
已将程序包“Microsoft.ML.OnnxRuntime.1.6.0”添加到文件夹“C:\Users\Administrator\source\repos\onnxDemo\packages”
已将程序包“Microsoft.ML.OnnxRuntime.1.6.0”添加到“packages.config”
已将“Microsoft.ML.OnnxRuntime 1.6.0”成功安装到 onnxDemo
执行 nuget 操作花费时间 7.18 sec
已用时间: 00:00:07.4252338
PM> Install-Package Microsoft.ML.OnnxRuntime.mklml -Source C:\Users\Administrator\AppData\Local\NuGet\Cache
正在尝试收集与目标为“native,Version=v0.0”的项目“onnxDemo”有关的包“Microsoft.ML.OnnxRuntime.mklml.1.6.0”的依赖项信息
收集依赖项信息花费时间 2 ms
正在尝试解析程序包“Microsoft.ML.OnnxRuntime.mklml.1.6.0”的依赖项,DependencyBehavior 为“Lowest”
解析依赖项信息花费时间 0 ms
正在解析操作以安装程序包“Microsoft.ML.OnnxRuntime.mklml.1.6.0”
已解析操作以安装程序包“Microsoft.ML.OnnxRuntime.mklml.1.6.0”
从“C:\Users\Administrator\AppData\Local\NuGet\Cache”检索包“Microsoft.ML.OnnxRuntime.MKLML 1.6.0”
正在将程序包“Microsoft.ML.OnnxRuntime.MKLML.1.6.0”添加到文件夹“C:\Users\Administrator\source\repos\onnxDemo\packages”
已将程序包“Microsoft.ML.OnnxRuntime.MKLML.1.6.0”添加到文件夹“C:\Users\Administrator\source\repos\onnxDemo\packages”
已将程序包“Microsoft.ML.OnnxRuntime.MKLML.1.6.0”添加到“packages.config”
已将“Microsoft.ML.OnnxRuntime.MKLML 1.6.0”成功安装到 onnxDemo
执行 nuget 操作花费时间 7.63 sec
已用时间: 00:00:07.6510274
PM>
(3)写代码,ONNXRuntime写代码十分简洁
大概分为三部分
1.初始化环境,会话等
2.会话中加载模型,得到模型的输入和输出节点
3.调用API得到模型的返回值
这里以语义分割模型U2Net为例
#include <assert.h>
#include <vector>
#include<ctime>
#include <onnxruntime_cxx_api.h>
int main(int argc, char* argv[])
{
//记录程序运行时间
auto start_time = clock();
//初始化环境,每个进程一个环境
//环境保留了线程池和其他状态信息
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "test");
//初始化Session选项
Ort::SessionOptions session_options;
session_options.SetIntraOpNumThreads(1);
// Available levels are
// ORT_DISABLE_ALL -> To disable all optimizations
// ORT_ENABLE_BASIC -> To enable basic optimizations (Such as redundant node removals)
// ORT_ENABLE_EXTENDED -> To enable extended optimizations (Includes level 1 + more complex optimizations like node fusions)
// ORT_ENABLE_ALL -> To Enable All possible opitmizations
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED);
//*************************************************************************
// 创建Session并把模型加载到内存中
const wchar_t* model_path = L"u2net.onnx";
printf("Using Onnxruntime C++ API\n");
Ort::Session session(env, model_path, session_options);
//*************************************************************************
//打印模型的输入层(node names, types, shape etc.)
Ort::AllocatorWithDefaultOptions allocator;
//输出模型输入节点的数量
size_t num_input_nodes = session.GetInputCount();
size_t num_output_nodes = session.GetOutputCount();
std::vector<const char*> input_node_names(num_input_nodes);
std::vector<const char*> output_node_names(num_output_nodes);
std::vector<int64_t> input_node_dims; // simplify... this model has only 1 input node {1, 3, 224, 224}.
// Otherwise need vector<vector<>>
printf("Number of inputs = %zu\n", num_input_nodes);
//迭代所有的输入节点
for (int i = 0; i < num_input_nodes; i++) {
//输出输入节点的名称
char* input_name = session.GetInputName(i, allocator);
printf("Input %d : name=%s\n", i, input_name);
input_node_names[i] = input_name;
// 输出输入节点的类型
Ort::TypeInfo type_info = session.GetInputTypeInfo(i);
auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
ONNXTensorElementDataType type = tensor_info.GetElementType();
printf("Input %d : type=%d\n", i, type);
input_node_dims = tensor_info.GetShape();
//输入节点的打印维度
printf("Input %d : num_dims=%zu\n", i, input_node_dims.size());
//打印各个维度的大小
for (int j = 0; j < input_node_dims.size(); j++)
printf("Input %d : dim %d=%jd\n", i, j, input_node_dims[j]);
//batch_size=1
input_node_dims[0] = 1;
}
//打印输出节点信息,方法类似
for (int i = 0; i < num_output_nodes; i++)
{
char* output_name = session.GetOutputName(i, allocator);
printf("Output: %d name=%s\n", i, output_name);
output_node_names[i] = output_name;
Ort::TypeInfo type_info = session.GetOutputTypeInfo(i);
auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
ONNXTensorElementDataType type = tensor_info.GetElementType();
printf("Output %d : type=%d\n", i, type);
auto output_node_dims = tensor_info.GetShape();
printf("Output %d : num_dims=%zu\n", i, output_node_dims.size());
for (int j = 0; j < input_node_dims.size(); j++)
printf("Output %d : dim %d=%jd\n", i, j, output_node_dims[j]);
}
//*************************************************************************
// 使用样本数据对模型进行评分,并检验出入值的合法性
size_t input_tensor_size = 3 * 320 * 320; // simplify ... using known dim values to calculate size
// use OrtGetTensorShapeElementCount() to get official size!
std::vector<float> input_tensor_values(input_tensor_size);
// 初始化一个数据(演示用,这里实际应该传入归一化的数据)
for (unsigned int i = 0; i < input_tensor_size; i++)
input_tensor_values[i] = (float)i / (input_tensor_size + 1);
// 为输入数据创建一个Tensor对象
try
{
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(memory_info, input_tensor_values.data(), input_tensor_size, input_node_dims.data(), 4);
//assert(input_tensor.IsTensor());
// 推理得到结果
auto output_tensors = session.Run(Ort::RunOptions{ nullptr }, input_node_names.data(), &input_tensor, 1, output_node_names.data(), 1);
assert(output_tensors.size() == 1 && output_tensors.front().IsTensor());
// Get pointer to output tensor float values
float* floatarr = output_tensors.front().GetTensorMutableData<float>();
printf("Number of outputs = %d\n", output_tensors.size());
}
catch (Ort::Exception& e)
{
printf(e.what());
}
auto end_time = clock();
printf("Proceed exit after %.2f seconds\n", static_cast<float>(end_time - start_time) / CLOCKS_PER_SEC);
printf("Done!\n");
return 0;
}
输出:
然后我们进一步进行一定程度的封装,方便我们后续使用
class U2NetModel
{
public:
U2NetModel(const wchar_t* onnx_model_path);
float* predict(std::vector<float>input_data,int batch_size=1);
private:
Ort::Env env;
Ort::Session session;
Ort::AllocatorWithDefaultOptions allocator;
std::vector<const char*>input_node_names;
std::vector<const char*>output_node_names;
std::vector<int64_t> input_node_dims;
};
U2NetModel::U2NetModel(const wchar_t* onnx_model_path):session(nullptr),env(nullptr)
{
//初始化环境,每个进程一个环境,环境保留了线程池和其他状态信息
this->env=Ort::Env(ORT_LOGGING_LEVEL_WARNING, "u2net");
//初始化Session选项
Ort::SessionOptions session_options;
session_options.SetInterOpNumThreads(1);
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
// 创建Session并把模型加载到内存中
this->session=Ort::Session(env, onnx_model_path,session_options);
//输入输出节点数量和名称
size_t num_input_nodes = session.GetInputCount();
size_t num_output_nodes = session.GetOutputCount();
for (int i = 0; i < num_input_nodes; i++)
{
auto input_node_name = session.GetInputName(i, allocator);
this->input_node_names.push_back(input_node_name);
Ort::TypeInfo type_info = session.GetInputTypeInfo(i);
auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
ONNXTensorElementDataType type = tensor_info.GetElementType();
this->input_node_dims = tensor_info.GetShape();
}
for (int i = 0; i < num_output_nodes; i++)
{
auto output_node_name = session.GetOutputName(i, allocator);
this->output_node_names.push_back(output_node_name);
}
}
float* U2NetModel::predict(std::vector<float>input_tensor_values,int batch_size)
{
this->input_node_dims[0] = batch_size;
auto input_tensor_size = input_tensor_values.size();
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(memory_info, input_tensor_values.data(), input_tensor_size, input_node_dims.data(), 4);
auto output_tensors = session.Run(Ort::RunOptions{ nullptr }, input_node_names.data(), &input_tensor, 1, output_node_names.data(), 1);
assert(output_tensors.size() == 1 && output_tensors.front().IsTensor());
float* floatarr = output_tensors.front().GetTensorMutableData<float>();
return floatarr;
}
然后初始化一个示例并调用接口
int main(int argc, char* argv[])
{
auto start_time = std::clock();
U2NetModel u2net(L"u2net.onnx");
size_t input_tensor_size = 3 * 320 * 320;
std::vector<float> input_tensor_values(input_tensor_size);
//初始化一个数据(演示用)
for (unsigned int i = 0; i < input_tensor_size; i++)
{
input_tensor_values[i] = (float)i / (input_tensor_size + 1);
}
float* results = nullptr;
try
{
results = u2net.predict(input_tensor_values);
}
catch (Ort::Exception& e)
{
delete results;
printf("%s\n", e.what());
}
auto end_time = std::clock();
printf("Proceed exits after %.2f seconds", static_cast<float>(end_time - start_time) / 1000);
printf("Done!\n");
return 0;
}
现在模型部分结束了,但问题是我们其实并没法得知我们的模型运行情况
所以我们还需要读入图片和显示图片,也就是
1.读入一张图片
2.模型推理
3.屏幕打印图片
现在我们仅仅完成了第二部
所以接下来我们需要安装OpenCV
OpenCV
待续…
下载OpenCV
opencv 下载:https://github.com/opencv/opencv/releases
文件复制
打开你的 vs目录 D:\Softs\Microsoft Visual Studio2019Community\VC\Tools\MSVC\14.23.28105\include 直接把opencv目录下该文件夹复制过去
打开你的 vs目录D:\Softs\Microsoft Visual Studio2019Community\VC\Tools\MSVC\14.23.28105\lib\x64 把这两个文件复制过去
打开 C:\Windows\System32和C:\Windows\SysWOW64这两个目录 把所有dll复制到这两个目录
设置VS2019
注意点 这两地方都选 x64
附加依赖项选自己的opencv版本
设置环境变量
win+R打开命令行,输入 SystemPropertiesAdvanced.exe,进行环境变量设置
写代码测试一下
#include <assert.h>
#include <vector>
#include <ctime>
#include <iostream>
#include <onnxruntime_cxx_api.h>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/videoio.hpp>
int main(int argc, char* argv[])
{
cv::Mat image = cv::imread("horse.jpg");
cv::resize(image, image, { 320, 320 },0.0,0.0, cv::INTER_CUBIC);
cv::imshow("test", image);
cv::waitKey(0);//不加这个会闪退
}
不过控制台上好像输出了一下动态链接失败的信息,但好像没影响到我现在的使用,就先暂时不管了hhh
借助OpenCV进行模型推理
首先是重载一个新的predict
函数来支持cv::Mat
数据
当然这里我新写的版本已经不直接返回float*
,而是std::vector<float>
class U2NetModel
{
public:
...
std::vector<float> predict(std::vector<float>& input_data,int batch_size=1,int index=0);
cv::Mat predict(cv::Mat& input_tensor, int batch_size = 1, int index = 0);
...
}
代码实现,增加了对于cv::Mat
的处理:
std::vector<float> U2NetModel::predict(std::vector<float>& input_tensor_values,int batch_size,int index)
{
this->input_node_dims[0] = batch_size;
this->output_node_dims[0] = batch_size;
float* floatarr = nullptr;
try
{
std::vector<const char*>output_node_names;
if (index != -1)
{
output_node_names = { this->output_node_names[index] };
}
else
{
output_node_names = this->output_node_names;
}
this->input_node_dims[0] = batch_size;
auto input_tensor_size = input_tensor_values.size();
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(memory_info, input_tensor_values.data(), input_tensor_size, input_node_dims.data(), 4);
auto output_tensors = session.Run(Ort::RunOptions{ nullptr }, input_node_names.data(), &input_tensor, 1, output_node_names.data(), 1);
assert(output_tensors.size() == 1 && output_tensors.front().IsTensor());
floatarr = output_tensors.front().GetTensorMutableData<float>();
}
catch (Ort::Exception&e)
{
throw e;
}
int64_t output_tensor_size = 1;
for (auto& it : this->output_node_dims)
{
output_tensor_size *= it;
}
std::vector<float>results(output_tensor_size);
for (unsigned i = 0;i < output_tensor_size; i++)
{
results[i] = floatarr[i];
}
return results;
}
cv::Mat U2NetModel::predict(cv::Mat& input_tensor, int batch_size, int index)
{
int input_tensor_size = input_tensor.cols * input_tensor.rows * 3;
std::size_t counter = 0;//std::vector空间一次性分配完成,避免过多的数据copy
std::vector<float>input_data(input_tensor_size);
std::vector<float>output_data;
try
{
for (unsigned k = 0; k < 3; k++)
{
for (unsigned i = 0; i < input_tensor.rows; i++)
{
for (unsigned j = 0; j < input_tensor.cols; j++)
{
input_data[counter++]=static_cast<float>(input_tensor.at<cv::Vec3b>(i, j)[k]) / 255.0;
}
}
}
}
catch (cv::Exception& e)
{
printf(e.what());
}
try
{
output_data = this->predict(input_data);
}
catch (Ort::Exception& e)
{
throw e;
}
cv::Mat output_tensor(output_data);
output_tensor=output_tensor.reshape(1, { 320,320 })*255.0;
std::cout << output_tensor.rows << " " << output_tensor.cols << "fuck" << std::endl;
return output_tensor;
}
int main(int argc, char* argv[])
{
U2NetModel model(L"u2net.onnx");
cv::Mat image = cv::imread("horse.jpg");
cv::resize(image, image, { 320, 320 },0.0,0.0, cv::INTER_CUBIC);//调整大小到320*320
cv::imshow("image", image); //打印原图片
cv::cvtColor(image, image, cv::COLOR_BGR2RGB); //BRG格式转化为RGB格式
auto result=model.predict(image); //模型预测
cv::imshow("result", result); //打印结果
cv::waitKey(0);
}
到这里模型的部署和结果的展示就OKK了
不过直接把模型输出转化为图片显然结果并不是非常理想
所以现在还需要对数据进行后处理,对图片进行二值化处理
得到一个Mask掩码矩阵
cv::Mat output_tensor(output_data);
output_tensor=255.0-output_tensor.reshape(1, { 320,320 })*255.0;
cv::threshold(output_tensor, output_tensor, 220, 255, cv::THRESH_BINARY_INV);
return output_tensor;
现在结果就已经十分的理想了
完整测试代码
#include <assert.h>
#include <vector>
#include <ctime>
#include <iostream>
#include <onnxruntime_cxx_api.h>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/videoio.hpp>
class U2NetModel
{
public:
U2NetModel(const wchar_t* onnx_model_path);
std::vector<float> predict(std::vector<float>& input_data,int batch_size=1,int index=0);
cv::Mat predict(cv::Mat& input_tensor, int batch_size = 1, int index = 0);
private:
Ort::Env env;
Ort::Session session;
Ort::AllocatorWithDefaultOptions allocator;
std::vector<const char*>input_node_names;
std::vector<const char*>output_node_names;
std::vector<int64_t> input_node_dims;
std::vector<int64_t> output_node_dims;
};
U2NetModel::U2NetModel(const wchar_t* onnx_model_path):session(nullptr),env(nullptr)
{
//初始化环境,每个进程一个环境,环境保留了线程池和其他状态信息
this->env=Ort::Env(ORT_LOGGING_LEVEL_WARNING, "u2net");
//初始化Session选项
Ort::SessionOptions session_options;
session_options.SetInterOpNumThreads(4);
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
// 创建Session并把模型加载到内存中
this->session=Ort::Session(env, onnx_model_path,session_options);
//输入输出节点数量和名称
size_t num_input_nodes = session.GetInputCount();
size_t num_output_nodes = session.GetOutputCount();
for (int i = 0; i < num_input_nodes; i++)
{
auto input_node_name = session.GetInputName(i, allocator);
this->input_node_names.push_back(input_node_name);
Ort::TypeInfo type_info = session.GetInputTypeInfo(i);
auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
ONNXTensorElementDataType type = tensor_info.GetElementType();
this->input_node_dims = tensor_info.GetShape();
}
for (int i = 0; i < num_output_nodes; i++)
{
auto output_node_name = session.GetOutputName(i, allocator);
this->output_node_names.push_back(output_node_name);
Ort::TypeInfo type_info = session.GetOutputTypeInfo(i);
auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
this->output_node_dims = tensor_info.GetShape();
}
}
std::vector<float> U2NetModel::predict(std::vector<float>& input_tensor_values,int batch_size,int index)
{
this->input_node_dims[0] = batch_size;
this->output_node_dims[0] = batch_size;
float* floatarr = nullptr;
try
{
std::vector<const char*>output_node_names;
if (index != -1)
{
output_node_names = { this->output_node_names[index] };
}
else
{
output_node_names = this->output_node_names;
}
this->input_node_dims[0] = batch_size;
auto input_tensor_size = input_tensor_values.size();
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(memory_info, input_tensor_values.data(), input_tensor_size, input_node_dims.data(), 4);
auto output_tensors = session.Run(Ort::RunOptions{ nullptr }, input_node_names.data(), &input_tensor, 1, output_node_names.data(), 1);
assert(output_tensors.size() == 1 && output_tensors.front().IsTensor());
floatarr = output_tensors.front().GetTensorMutableData<float>();
}
catch (Ort::Exception&e)
{
throw e;
}
int64_t output_tensor_size = 1;
for (auto& it : this->output_node_dims)
{
output_tensor_size *= it;
}
std::vector<float>results(output_tensor_size);
for (unsigned i = 0;i < output_tensor_size; i++)
{
results[i] = floatarr[i];
}
return results;
}
cv::Mat U2NetModel::predict(cv::Mat& input_tensor, int batch_size, int index)
{
int input_tensor_size = input_tensor.cols * input_tensor.rows * 3;
std::size_t counter = 0;//std::vector空间一次性分配完成,避免过多的数据copy
std::vector<float>input_data(input_tensor_size);
std::vector<float>output_data;
try
{
for (unsigned k = 0; k < 3; k++)
{
for (unsigned i = 0; i < input_tensor.rows; i++)
{
for (unsigned j = 0; j < input_tensor.cols; j++)
{
input_data[counter++]=static_cast<float>(input_tensor.at<cv::Vec3b>(i, j)[k]) / 255.0;
}
}
}
}
catch (cv::Exception& e)
{
printf(e.what());
}
try
{
output_data = this->predict(input_data);
}
catch (Ort::Exception& e)
{
throw e;
}
cv::Mat output_tensor(output_data);
output_tensor=255.0-output_tensor.reshape(1, { 320,320 })*255.0;
cv::threshold(output_tensor, output_tensor, 220, 255, cv::THRESH_BINARY_INV);
return output_tensor;
}
int main(int argc, char* argv[])
{
U2NetModel model(L"u2net.onnx");
cv::Mat image = cv::imread("horse.jpg");
cv::resize(image, image, { 320, 320 },0.0,0.0, cv::INTER_CUBIC);//调整大小到320*320
cv::imshow("image", image); //打印原图片
cv::cvtColor(image, image, cv::COLOR_BGR2RGB); //BRG格式转化为RGB格式
auto result=model.predict(image); //模型预测
cv::imshow("result", result); //打印结果
cv::waitKey(0);
}