【飛槳開發者說】李康宇,PPDE飛槳開發者技術專家,工做於機械科學研究總院,視覺研發工程師php
上一篇文章《使用PaddleX高效實現指針型表計讀取(一)》中介紹了很是好用的深度學習開發工具PaddleX,以壓力錶分割爲例,闡述了PaddleX 圖形化開發界面的使用方法。html
本文介紹如何將飛槳的C++預測代碼生成爲Visual Studio下的解決方案,以及最關鍵的,如何將C++預測代碼生成爲可調用的動態連接庫dll,打通真正能夠工業實戰的開發流程。ios
下載安裝命令 ## CPU版本安裝命令 pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle ## GPU版本安裝命令 pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu
本章目錄:git
-
使用CMake編譯PaddleX C++預測代碼生成本地化工程文件。github
-
將工程文件轉化成具有輸入輸出接口的DLL文件。windows
-
使用C#編寫界面,調用DLL實現壓力錶分割。數組
使用CMake編譯PaddleX 預測代碼生成本地化工程文件
1. 準備工做ide
安裝CMake 3.16.5,Visual Studio 2019,OpenCV 3.4.6三個軟件。函數
下載PaddleX develop分支的預測代碼:工具
https://github.com/PaddlePaddle/PaddleX
根據本身的CUDA和cuDNN版本,對應下載PaddlePaddle官方提供的預編譯Windows預測庫,我所測試的版本爲cuda10.0_cudnn7_avx_mkl,其餘版本未測試。
預編譯 Windows 預測庫下載地址:
https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/inference_deployment/inference/windows_cpp_inference.html
將上述下載的預測庫fluid_inference.zip,與OpenCV和PaddleX三個文件夾放在同一個路徑下,方便操做。
將Opencv的bin文件路徑添加至系統變量Path中:
2. CMake編譯
打開PaddleX-develop/deploy/cpp路徑下的CMakeLists.txt,將其中的:
add_executable(segmenter demo/segmenter.cpp src/transforms.cpp src/paddlex.cpp src/visualize.cpp)
改成:
ADD_library(segmenter SHARED demo/segmenter.cpp src/transforms.cpp src/paddlex.cpp src/visualize.cpp)
打開CMake:
-
source code源碼路徑選爲PaddleX-develop/ deploy/cpp所在目錄;
-
在PaddleX-develop/ deploy/cpp下新建文件夾build_out,用於存儲編譯後的文件;
-
選擇好路徑後,點擊Configure。
將生成器指定爲Visual Studio 2019,x64:
點擊Finish,此時會出現報錯,這是由於沒有設置CUDA_LIB、OPENCV_DIR和PADDLE_DIR:
按照下圖:
-
將CUDA_LIB、OPENCV_DIR和PADDLE_DIR的路徑添加進去;
-
點擊Configure;
-
點擊Generate。
在Configuring done和Generating done後,點擊Open Project,即會自動用Visual Studio 2019打開本地化工程文件。
2. 將工程文件轉化成具有輸入輸出接口的DLL文件
接下來打開編譯PaddleX生成的本地化工程文件,由於我要作的是分割任務,涉及到其中的segmenter部分。
右鍵segmenter,查看其屬性。
-
將配置類型改成動態庫;
-
指定DLL的輸出目錄;
-
確認配置爲Release,平臺爲x64。
配置好後,接下來是修改segmenter.cpp代碼(這裏先不講爲何這麼修改,下一小節會詳細解釋):
#include <glog/logging.h> #include <omp.h> #include <algorithm> #include <chrono> // NOLINT #include <fstream> #include <iostream> #include <string> #include <vector> #include <utility> #include "include/paddlex/paddlex.h" #include "include/paddlex/visualize.h" extern "C" __declspec(dllexport) cv::Mat* LoadModel(char *input, int width, int height); __declspec(dllexport) cv::Mat* LoadModel(char* input, int width, int height) { std::string model_dir = "C:\\Users\\Admin\\Desktop\\inference_model"; std::string key = ""; int gpu_id = 0; bool use_trt = 0; bool use_gpu = 0; PaddleX::SegResult result; cv::Mat im(height, width, CV_8UC3, input); //加載模型及建立分割 PaddleX::Model model; model.Init(model_dir, use_gpu, use_trt, gpu_id, key); model.predict(im, &result); //結果返回 cv::Mat vis_img = PaddleX::Visualize(im, result, model.labels); return new cv::Mat(vis_img); }
修改好上述內容後,右鍵 ==> 僅用於項目 ==> 僅從新生成segmenter
生成成功後,就能夠在以前指定的輸出目錄中看到生成的DLL文件了
3. 使用C#編寫界面,調用DLL實現壓力錶分割
工業上通常使用C#來開發用戶界面,所以須要將上述工程文件生成爲在從C#中可調用的。無論是作目標檢測仍是語義分割,咱們都須要將圖像輸入至模型中,而後將預測的結果輸出。在本節中,我以壓力錶的語義分割爲例,介紹如何調用具備輸入和輸出接口的DLL文件(在本例中,輸入和輸出均爲圖像)。
打開Visual studio 2019,建立一個Windows窗體應用
在窗體界面,設置一個Button控件和兩個Picturebox控件
在C#中,咱們使用Bitmap類將對圖像進行操做,用於加載指定路徑下的圖像,可是Bitmap類並不適用於C++中。因此須要解決的問題是如何正確地從C#中傳遞圖像數據到C++端,而後再將C++中分割後的結果傳回C#中。
也就是說,須要解決的問題有兩個:
問題一:如何將C#中圖像數據傳遞至C++;
問題二:如何在C++中接收圖像數據,並將分割結果返回至C#。
下面先將C#的代碼列出,再一一說明這兩個問題:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Runtime.InteropServices; using OpenCvSharp; namespace PaddleX_dll_test { public partial class Form1 : Form { public Form1() { InitializeComponent(); } [DllImport("segmenter.dll", EntryPoint = "LoadModel", SetLastError = true, CharSet = CharSet.Ansi)] static extern IntPtr LoadModel(byte[] input, int height, int width); //out IntPtr seg_res private void Button1_Click(object sender, EventArgs e) { string image_path = "C:/Users/Admin/Desktop/yalibiao_126.JPG"; Bitmap bmp = new Bitmap(image_path); pictureBox1.Image = bmp; int stride; byte[] source = GetBGRValues(bmp, out stride); IntPtr seg_img = LoadModel(source, bmp.Width, bmp.Height); //out seg_img Mat img = new Mat(seg_img); Bitmap seg_show = new Bitmap(img.Cols, img.Rows, (int)img.Step(), System.Drawing.Imaging.PixelFormat.Format24bppRgb, img.Data); pictureBox2.Image = seg_show; } // 將Btimap類轉換爲byte[]類函數 public static byte[] GetBGRValues(Bitmap bmp, out int stride) { var rect = new Rectangle(0, 0, bmp.Width, bmp.Height); var bmpData = bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, bmp.PixelFormat); stride = bmpData.Stride; var rowBytes = bmpData.Width * Image.GetPixelFormatSize(bmp.PixelFormat) / 8; var imgBytes = bmp.Height * rowBytes; byte[] rgbValues = new byte[imgBytes]; IntPtr ptr = bmpData.Scan0; for (var i = 0; i < bmp.Height; i++) { Marshal.Copy(ptr, rgbValues, i * rowBytes, rowBytes); ptr += bmpData.Stride; } bmp.UnlockBits(bmpData); return rgbValues; } } }
問題一:爲了解決該問題,咱們能夠在C#中將Bitmap類轉換爲byte[]類,再傳遞給C++去處理。這一部分涉及的代碼爲:
// C# 代碼 //也可設置爲可選路徑,我這裏就直接指定了 string image_path = "C:/Users/Admin/Desktop/yalibiao_126.JPG"; Bitmap bmp = new Bitmap(image_path); int stride; byte[] source = GetBGRValues(bmp, out stride); // 類型轉換 bitmap ==> byte[] ... // 將Btimap類轉換爲byte[]類 public static byte[] GetBGRValues(Bitmap bmp, out int stride) { var rect = new Rectangle(0, 0, bmp.Width, bmp.Height); var bmpData = bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, bmp.PixelFormat); stride = bmpData.Stride; var rowBytes = bmpData.Width * Image.GetPixelFormatSize(bmp.PixelFormat) / 8; var imgBytes = bmp.Height * rowBytes; byte[] rgbValues = new byte[imgBytes]; IntPtr ptr = bmpData.Scan0; for (var i = 0; i < bmp.Height; i++) { Marshal.Copy(ptr, rgbValues, i * rowBytes, rowBytes); ptr += bmpData.Stride; } bmp.UnlockBits(bmpData); return rgbValues; }
經過上述代碼,便可將指定路徑下的Bitmap類圖像轉爲byte[]字節數組的類型。
問題二:在C++中,咱們須要將接收到的byte[]類型數據轉換成易操做的OpenCV Mat類型。爲了還原圖像,須要用到圖像的byte[]數據、長、寬和通道數。因爲我所用的圖像通道數已知,就只把byte[]數據、長、寬三個數據傳到LoadModel中。而後經過指針的方式將分割後的圖像返回至C#中。這一部分涉及的代碼爲:
//C#代碼 static extern IntPtr LoadModel(byte[] input, int height, int width); // LoadModel的類型爲IntPtr ... IntPtr seg_img = LoadModel(source, bmp.Width, bmp.Height);// 傳遞圖像數據:byte[]數組、長、寬,並接收返回值 ... //C++代碼 extern "C" __declspec(dllexport) cv::Mat* LoadModel(char *input, int width, int height);//聲明爲C編譯、鏈接方式的外部函數 __declspec(dllexport) cv::Mat* LoadModel(char* input, int width, int height) // 經過地址返回Mat類型的分割圖像結果 ... cv::Mat im(height, width, CV_8UC3, input); // 由byte[]數組、長、寬和通道數生成Mat類型圖像
至此,已經用C#寫好窗體應用程序。
在運行前,須要將segmenter.dll目錄下的所有文件及其lib文件複製到C#項目的運行目錄bin/Debug目錄下
其中有幾個文件只有dll,沒有對應的lib文件,這個時候,咱們須要在Paddle預測庫文件中找到以下的lib文件,這裏推薦直接使用everything之類的工具搜索獲取。
複製徹底部文件後,點擊啓動進行測試。能夠看到,界面左邊是輸入的原始圖片,右邊是通過C++代碼分割後返回的圖片。這也說明咱們成功地生成了具備輸入和輸出接口的DLL文件。
都看到這裏了,還不點個贊,關注一下?謝謝你們!最後,再一次歡迎你們給這款好用的工具點個star!
PaddleX Github連接:
https://github.com/PaddlePaddle/PaddleX
如在使用過程當中有問題,可加入飛槳官方QQ羣進行交流:1108045677。
下載安裝命令 ## CPU版本安裝命令 pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle ## GPU版本安裝命令 pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu