【飞桨开发者说】李康宇,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