工业党福利:使用PaddleX高效实现指针型表计读取(二)

原创
10/17 23:00
阅读数 32

飞桨开发者说】李康宇,PPDE飞桨开发者技术专家,工作于机械科学研究总院,视觉研发工程师


上一篇文章《使用PaddleX高效实现指针型表计读取(一)》中介绍了非常好用的深度学习开发工具PaddleX,以压力表分割为例,阐述了PaddleX 图形化开发界面的使用方法。

 

本文介绍如何将飞桨的C++预测代码生成为Visual Studio下的解决方案,以及最关键的,如何将C++预测代码生成为可调用的动态链接库dll,打通真正可以工业实战的开发流程。

下载安装命令

## 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

本章目录:

  1. 使用CMake编译PaddleX C++预测代码生成本地化工程文件。

  2. 将工程文件转化成具备输入输出接口的DLL文件。

  3. 使用C#编写界面,调用DLL实现压力表分割。

 

使用CMake编译PaddleX 预测代码生成本地化工程文件

 

1. 准备工作

安装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
展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部