工业党福利:使用PaddleX高效实现指针型表计读取系列文章(2)

时间:2022-07-24
本文章向大家介绍工业党福利:使用PaddleX高效实现指针型表计读取系列文章(2),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

最近在做一个工业巡检的项目,主要涉及的内容是指针型表计的读取。本系列文章主要介绍实现表计读取的全流程开发(立个FLAG,想想真是肝...留下了不争气的眼泪),其中主要使用的工具为百度开发的PaddleX和Visual studio 2019。

一般来讲,在工业领域使用深度学习技术来实施的项目主要为工业质检和工业巡检两部分,实现这两部分的流程均为:

本系列文章的内容包含了上述流程的全部内容,其目录如下:

  1. 高效语义分割:基于PaddleX零代码快速实现表计分割
  2. 工业场景适配:Windows下PaddleX的C++编译并生成dll
  3. 表计读取实战:基于C#的识别界面开发和dll文件调用

正文开始前再说一句,各位兄弟姐妹快去GitHub给paddleX点star啊。点star,不白嫖!

由于公众号中很难添加超链接,大家可以点击阅读原文,查看发在知乎上的文章。


正文开始:

二、工业场景适配:Windows下PaddleX的C++编译并生成dll

本节目录

  1. 使用CMake编译PaddleX C++文件生成本地化工程文件
  2. 生成开放输入输出接口的DLL文件
  3. 使用C#编写界面,调用DLL实现压力表分割

1. 使用CMake编译PaddleX C++文件生成本地化工程文件

1.1 准备工作

安装CMake 3.16.5,VisualStudio 2019,OpenCV 3.4.6三个软件。

下载develop分支下的预测代码:https://github.com/PaddlePaddle/PaddleX

根据自己的CUDA和cuDNN版本,下载相应的Paddle官方提供的Windows预测库,我所测试的版本为cuda10.0_cudnn7_avx_mkl,其他版本未测试。

链接为:

https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/inference_deployment/inference/windows_cpp_inference.html

将上述下载的OpenCV、fluid_inference_install_dir.zip、PaddleX-develop三个文件放在同一个路径下,方便操作。

将Opencv的bin文件路径添加至系统变量Path中:

1.2 CMake编译

打开deploy/cpp路径下的CMakeLists.txt,将其中的:

add_executable(segmenter demo/segmenter.cpp src/transforms.cppsrc/paddlex.cpp src/visualize.cpp)

改为:

ADD_library(segmenter SHARED demo/segmenter.cpp src/transforms.cpp src/paddlex.cpp src/visualize.cpp)

打开CMake:①sourcecode源码路径选为PaddleX-develop中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和Generatingdone后,点击Open Project,即会自动用VisualStudio 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文件。

以上,就是《使用PaddleX高效实现指针型表计读取系列文章》第二篇的全部内容。