工業黨福利:使用PaddleX高效實現指針型表計讀取(二)

飛槳開發者說】李康宇,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

  1. 使用CMake編譯PaddleX C++預測代碼生成本地化工程文件。github

  2. 將工程文件轉化成具有輸入輸出接口的DLL文件。windows

  3. 使用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:

  1. source code源碼路徑選爲PaddleX-develop/ deploy/cpp所在目錄;

  2. PaddleX-develop/ deploy/cpp下新建文件夾build_out,用於存儲編譯後的文件;

  3. 選擇好路徑後,點擊Configure。

將生成器指定爲Visual Studio 2019,x64:

點擊Finish,此時會出現報錯,這是由於沒有設置CUDA_LIB、OPENCV_DIR和PADDLE_DIR:

按照下圖:

  1. 將CUDA_LIB、OPENCV_DIR和PADDLE_DIR的路徑添加進去;

  2. 點擊Configure;

  3. 點擊Generate。

在Configuring done和Generating done後,點擊Open Project,即會自動用Visual Studio 2019打開本地化工程文件。

2. 將工程文件轉化成具有輸入輸出接口的DLL文件

接下來打開編譯PaddleX生成的本地化工程文件,由於我要作的是分割任務,涉及到其中的segmenter部分。

右鍵segmenter,查看其屬性。

  1. 將配置類型改成動態庫;

  2. 指定DLL的輸出目錄;

  3. 確認配置爲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(00, 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(00, 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
相關文章
相關標籤/搜索