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);
}