一文掌握面向Windows平台的深度学习工控程序开发(使用Paddle Inference部署MFC、C#程序,内含完整代码链接)

2023-05-16

目录

  • 一、概述
    • 1.1 智能制造和飞桨
    • 1.2 Paddle Inference工业级应用部署工具
  • 二、算法训练和导出
    • 2.1 任务概述和实现原理
    • 2.2 训练和静态模型导出
  • 三、部署环境准备
  • 四、Windows下C++工程编译和运行
    • 4.1 工程创建
    • 4.2 配置OpenCV
    • 4.3 配置Paddle Inference、cuda和tensorrt
    • 4.4 核心代码分析
    • 4.5 完整推理
  • 五、MFC工程调用
    • 5.1 基于C++的dll制作
    • 5.2 MFC工程中调用
  • 六、C#工程调用
    • 6.1基于C++的dll制作
    • 6.2 C#中调用dll
  • 七、完整代码链接

一、概述

1.1 智能制造和飞桨

制造业作为国民经济主体,是国家创造力、竞争力和综合国力的重要体现。作为制造强国建设的主攻方向,智能制造发展水平关乎我国未来制造业的全球地位。与此同时,面对供应链环境不确定性的增加、人力等运营成本的逐渐攀升、“双碳”战略之下能源转型的迫切要求,制造业想要实现高质量发展,迈向中高端水平,不仅需要从低附加价值领域向高附加价值领域两端延伸,更重要是需要加快人工智能等核心技术规模化应用落地。在此背景之下,如何利用好人工智能这把利剑,加快新旧动能转换,实现传统生产方式的转型升级,也成为每个制造企业不得不思考的问题。目前,在AI工业大生产阶段,深度学习技术的通用性越来越强,深度学习平台的标准化、自动化和模块化特征越来越显著,深度学习应用越来越广泛且深入,已经遍地开花。

目前,以飞桨为代表的人工智能平台在制造业的落地主要集中在工业视觉、工业设备监控、数据智能和物流仓储等应用场景,在研发设计、优化生产工艺和排期、设备运维、智能供应链等环节发挥着“智眼”和“大脑”的支撑作用。工业视觉检测作为保障产品质量的重要环节,被广泛应用在钢铁、汽车、3C 电子、印染纺织等众多领域。在AI出现之前,往往是依赖人工检测或者使用传统图像处理算法。人工检测效率低,成本高,且容易收到认为主管因素影响,传统图像处理算法对于复杂场景鲁棒性差,而随着卷积神经网络为代表的AI算法出现,有效地解决在复杂场景检测的能力,在实际的项目过程中对目标识别具有更好的普适性。

1.2 Paddle Inference工业级应用部署工具

在工业级深度学习实践领域中,我们经常能听到一种说法——模型部署是打通AI应用的最后一公里!想要走通这一公里,看似简单,但是真正实践起来却困难重重。显卡利用率低、内存溢出、多线程调度奔溃、tensorrt加速算子不支持等等问题一直是深度学习模型最后部署的老大难问题。这时,我们就可以选择Paddle Inference部署工具。

Paddle Inference 是飞桨的原生推理库,可以提供高性能的工业生产级推理能力。一般的企业级部署通常会追求更极致的部署性能,且希望能够在生产环境安装一个不包含后向算子,且比主框架更轻量的预测库,Paddle Inference应运而生。Paddle Inference提取了主框架的前向算子,可以无缝支持所有主框架训练好的模型,且通过内存复用、算子融合等大量优化手段,并整合了主流的硬件加速库如Intel的oneDNN、NVIDIA的TensorRT等, 提供用户最极致的部署性能。此外还封装C/C++的预测接口,使生产环境更便利多样。

有了这样一套部署工具,我们开发工业智能产品就非常简单了。一般的,我们可以基于Python语言使用PaddlePaddle来实现模型训练(可以使用一些套件库来加速模型研发,例如PaddleClas、PaddleDetection、PaddleSeg等),然后再使用C++语言利用Paddle Inference库实现工业生产环境的高效稳定部署。

本篇博文重点介绍如何利用飞桨Paddle Inference工具在windows 10平台上实现工业级深度学习应用部署,对相关的算法原理只做基本介绍。

二、算法训练和导出

2.1 任务概述和实现原理

本教程使用PP-LiteSeg模型对工业质检场景下的缺陷进行精细分割,实现像素级的工业缺陷检测任务。下图左边是原图,右边是分割图片,缺陷区域使用绿色表示,其他区域使用红色表示。
任务概述
PP-LiteSeg模型是PaddleSeg团队自研的轻量级语义分割模型,模型结构如下。
算法模型原理
PP-LiteSeg模型更详细的原理介绍请参考官网链接。

2.2 训练和静态模型导出

本项目使用的工业质检瑕疵分割数据集,包含3类目标和1类背景。其中训练集:691张图像,验证集:86张图像,测试集:87张图像。数据集格式如下:

defect_data
├── Annotations
├── JPEGImages
├── test.txt
├── train.txt
└── val.txt

完整的训练、推理和导出代码在Ai Studio上已给出(链接),读者只需要folk即可运行。
在实际工业使用时,可以根据模型的大小以及速度要求来选型,然后只需要替换模型配置参数重新训练即可。因此,使用PaddlePaddle的相关算法套件可以很快速的完成模型开发、训练和验证工作。
整个训练时间大概耗时1小时,最终推理结果如下所示:
在这里插入图片描述
本项目实例在yml文件中iters设置为8000,在实际测试时发现远没有到达最佳精度位置(mIoU=0.5917),可以增加iters延长训练时间来获得更高的检测精度。尽管如此,从推理结果上看整体检测效果还是可以的,基本能够检测出对应的缺陷区域。

为了方便后面进行工业级的部署,PaddleSeg提供了一键动转静的功能,即将训练出来的动态图模型文件转化成静态图形式(只有转成静态图模型才能用C++推理)。

  • 最终结果文件

output
├── deploy.yaml # 部署相关的配置文件
├── model.pdiparams # 静态图模型参数
├── model.pdiparams.info # 参数额外信息,一般无需关注
└── model.pdmodel # 静态图模型文件

如果读者想深入学习如何根据算法建模、如何调参、如何高效训练等技术,请参考飞桨语义分割官方教程.(官方教程包含的案例非常丰富且步骤详细,本文不再赘述)。

三、部署环境准备

我们最终需要使用Paddle Inference工具将前面导出的模型实现Windows平台上的C++推理。因此,我们首先需要配置基本的PaddleInference环境。Paddle Inference官方下载网址。在下载C++预测库的时候,我们需要记住对应的当前版本配置环境。
如下图所示:
Paddle Inference下载
考虑到版本的适应性,我们可以选择vx_mkl_cuda10.2_cudnn7.6.5_avx_mkl-trt7.0.0.11进行下载使用。这个版本对应的cuda是10.2,cudnn是7.6.5,tensorrt库是7.0.0.11。其中尤其需要注意cuda和cudnn,如果这个预测库版本需要的cuda和cudnn跟我们当前电脑已经装好的cuda和cudnn版本不一致,并且在预编译好的预测库中没有我们当前电脑环境的版本,那么只有两种解决方法:一种就是卸载掉当前cuda和cudnn重新安装适配版本;另一种就是按照官方教程自行编译paddle inference。一般来说,自行编译paddle inference会遇到不少问题,这种解决方案的代价比较大。如果cuda和cudnn版本不一致,个人建议还是重装cuda和cudnn会更方便一些。安装好以后我们在C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.2\lib\x64目录下(此处注意版本的一致性,我们下载的paddle_inference是cuda10.2的,所以这里引用的cuda目录也要对应着10.2),然后将其中的所有的lib文件复制出来,复制到d:/toolplace下的cuda目录,这个cuda目录就是专门用于存放cuda的lib文件的

另外,本文需要使用opencv加载和预处理图像,因此需要安装好opencv。这里可以选择比较新的OpenCV4.5.5对应的windows版本下载,下载后运行解压,将opencv文件夹放置在统一的名为toolplace的目录下。

全部下载好以后我们可以将所有的环境库单独放置在统一的名为toolplace的目录下,方便我们后期配置,如下图所示:
在这里插入图片描述

四、Windows下C++工程编译和运行

4.1 工程创建

本小节将使用VS2019来创建一个C++控制台工程,名称为PaddleDemo,在这个工程里面实现基于C++的工业瑕疵分割推理。如下图所示:
在这里插入图片描述
创建完成后我们重新设置生成项目为Debug,并且是64位(必须是构建64位程序),如下图所示:
在这里插入图片描述
然后将项目编译运行一下确保Visual Studio基本环境没有问题。

接下来进行项目配置。

4.2 配置OpenCV

首先配置一下opencv使得能够正常的在程序中加载图像。单击菜单栏“项目”-“属性”,然后单击左侧“VC++目录”,在右边包含目录中添加如下路径:

D:\toolplace\opencv\build\include
D:\toolplace\opencv\build\include\opencv2

在库目录中添加:

D:\toolplace\opencv\build\x64\vc15\lib

如下图所示:
在这里插入图片描述
然后单击左侧“链接器”—“输入”,在右侧附加依赖项中添加opencv对应的lib文件:

opencv_world455d

如下图所示:
在这里插入图片描述
注意这里我们链接的是debug版本的opencv库,如果我们生成的是release版本的,则需要链接opencv_world455.lib文件。

最后将D:\toolplace\opencv\build\x64\vc15\bin目录下的opencv_world455d.dll文件拷贝到PaddleDemo工程的根目录下面。然后我们找张测试图片,命名为test.png也放置在PaddleDemo工程的根目录下面,如下图所示:
在这里插入图片描述
下面打开PaddleDemo.cpp主文件,编写C++图像调用代码测试下:

//导入系统库
#include <iostream> 

//导入opencv库
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp>  

using namespace cv;

int main()
{
	Mat img = imread("test.png",cv::IMREAD_COLOR);
	namedWindow("test");
	imshow("test", img);
	waitKey(6000);
	return 0;
}

按ctrl+F5运行,如果没有问题会显示test.png图片,如下图所示:
在这里插入图片描述

4.3 配置Paddle Inference、cuda和tensorrt

本小节我们将来配置Paddle Inference并使用GPU进行推理。Paddle Inference文件夹中主要包含paddle原生推理库和third_party第三方依赖库。由于依赖库比较多,我们可以抽取必要的进行引入。但是在初期调试的时候,建议将所有依赖库全部引入进来,等后期完成开发了再逐步剔除,这样不容易出问题。

单击菜单栏“项目”-“PaddleDemo属性”,打开属性页面,然后在左侧单击“VC++目录”,在右侧“包含目录”中继续添加如下路径:
在这里插入图片描述
具体修改方式对照着自己的包存放路径来设置。然后在库目录里面添加如下库:
在这里插入图片描述
最后,单击左侧“链接器”—“输入”,在附加依赖项中输入如下lib文件:

opencv_world455.lib
paddle_inference.lib
cryptopp-static.lib
glog.lib
gflags_static.lib
mkldnn.lib
libiomp5md.lib
mklml.lib
onnxruntime.lib
paddle2onnx.lib
libprotobuf.lib
utf8proc_static.lib
xxhash.lib
cudart.lib
cublas.lib
cudnn.lib
myelin64_1.lib
nvinfer.lib
nvinfer_plugin.lib
nvonnxparser.lib
nvparsers.lib

为了能够在工程中运行深度学习模型,我们将前面动转静得到的model.pdiparams和model.pdmodel放置在PaddleDemo项目根目录下的model文件夹中,然后将前面各个配置文件夹下面的dll文件也拷贝到当前项目根目录下面,最后根目录文件如下所示:
在这里插入图片描述

4.4 核心代码分析

  • 库引用
//导入系统库
#include <iostream> 

//导入opencv库
#include <opencv2/opencv.hpp> 
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp> 

//导入paddle库
#include <paddle_inference_api.h>

using namespace cv;

上述代码主要引入paddle inference对应的库头文件paddle_inference_api.h以及opencv库对应的三个文件,最后定义一下命名空间cv。

  • 定义全局变量
//模型预测器
std::shared_ptr<paddle_infer::Predictor> g_predictor;

//目标推理图像对应的宽高
int target_width = 512;
int target_height = 512;

// 归一化对应的均值和方差
std::vector<float> g_fmean;
std::vector<float> g_fstd;

上述全局变量中最关键的是定义了全局的深度学习预测器g_predictor,这是一个paddle_infer::Predictor类的指针变量,后面所有的推理都需要这个变量。由于我们真实工业场景中加载一次模型是比较耗时的,因此,一般情况下我们定义这样一个全局变量,在程序初始化时加载一次,后面就可以一直使用这个加载后的模型预测器进行推理,直到程序退出才释放这个模型。

  • 主函数main
    主函数部分首先使用自定义的init函数初始化深度学习推理环境,然后读取一张待预测图片,然后交给predict函数进行预测,由于我们这个模型是一个语义分割模型,输入是图片,输出也是图片,因此这里的predict函数输出是mask,最后保存mask图片即可。
int main()
{
	//初始化环境
	init();
	
	//加载图像和预处理
	Mat img = imread("test.png",cv::IMREAD_COLOR);
	Mat mask = Mat(img.rows, img.cols, CV_8UC1);

	//开始推理
	predict(img, mask);

	//保存掩码结果
	imwrite("result.jpg", mask);
	std::cout << "处理完成" << std::endl;

	return 0;
}
  • 初始化深度学习推理环境init
    参照paddle inference官网,初始化深度学习部署环境主要就是创建配置器config,然后通过config.SetModel将训练好的静态图模型导入。如果使用GPU预测可以使用config.EnableUseGpu和config.EnableMemoryOptim来设置。考虑到后期tensorrt加速,可以使用config.EnableTensorRtEngine来开启tensorrt,但是刚开始的时候建议不要开启tensorrt,因此有可能会推导不成功,后期需要使用config.SetTRTDynamicShapeInfo来设置关键节点的动态图形状才能保证tensorrt正常推理。最后,我们手工赋值一下归一化需要使用的均值和方差。
void init() {
	// 创建默认配置对象
	paddle_infer::Config config;
	config.SetModel("model/model.pdmodel", "model/model.pdiparams");
	config.EnableUseGpu(100, 0);
	config.EnableMemoryOptim();

	//开启tensorrt加速
	/*config.EnableTensorRtEngine(1 << 20, 1, 10,paddle_infer::PrecisionType::kFloat32, true, false);

	std::map<std::string, std::vector<int>> min_input_shape = {
			  {"x", {1, 3, 512, 512}},
			  {"bilinear_interp_v2_4.tmp_0", {1, 128, 32, 32}},	
			  {"bilinear_interp_v2_5.tmp_0", {1, 96, 64, 64}},
			  {"max_2.tmp_0", {1, 1, 32, 32}},	
			  {"bilinear_interp_v2_3.tmp_0", {1, 128, 16, 16}},
			  {"max_0.tmp_0", {1, 1, 16, 16}},
			  {"max_3.tmp_0", {1, 1, 32, 32}},
			  {"max_1.tmp_0", {1, 1, 16, 16}},
			  {"mean_0.tmp_0", {1, 1, 16, 16}},
			  {"relu_54.tmp_0", {1, 128, 16, 16}}, 
			  {"relu_60.tmp_0", {1, 96, 64, 64}},
			  {"max_4.tmp_0", {1, 1, 64, 64}},
			  {"max_5.tmp_0", {1, 1, 64, 64}},
			  {"mean_4.tmp_0", {1, 1, 64, 64}},
			  {"mean_2.tmp_0", {1, 1, 32, 32}},
			   {"relu_57.tmp_0", {1, 128, 32, 32}},
	};
	std::map<std::string, std::vector<int>> max_input_shape = {
		{"x", {1, 3, 512, 512}},
		{"bilinear_interp_v2_4.tmp_0", {1, 128, 32, 32}},
		{"bilinear_interp_v2_5.tmp_0", {1, 96, 64, 64}},
		 {"max_2.tmp_0", {1, 1, 32, 32}},
		 {"bilinear_interp_v2_3.tmp_0", {1, 128, 16, 16}},
		 {"max_0.tmp_0", {1, 1, 16, 16}},
		 {"max_3.tmp_0", {1, 1, 32, 32}},
		 {"max_1.tmp_0", {1, 1, 16, 16}},
		 {"mean_0.tmp_0", {1, 1, 16, 16}},
		 {"relu_54.tmp_0", {1, 128, 16, 16}},
		 {"max_4.tmp_0", {1, 1, 64, 64}},
		 {"max_5.tmp_0", {1, 1, 64, 64}},
		 {"mean_4.tmp_0", {1, 1, 64, 64}},
		 {"relu_60.tmp_0", {1, 96, 64, 64}},
		 {"mean_2.tmp_0", {1, 1, 32, 32}},
		 {"relu_57.tmp_0", {1, 128, 32, 32}},
	};
	std::map<std::string, std::vector<int>> opt_input_shape = {
		{"x", {1, 3, 512, 512}},
		{"bilinear_interp_v2_4.tmp_0", {1, 128, 32, 32}},
		{"bilinear_interp_v2_5.tmp_0", {1, 96, 64, 64}},
		 {"max_2.tmp_0", {1, 1, 32, 32}},
		 {"bilinear_interp_v2_3.tmp_0", {1, 128, 16, 16}},
		 {"max_0.tmp_0", {1, 1, 16, 16}},
		 {"max_3.tmp_0", {1, 1, 32, 32}},
		 {"max_1.tmp_0", {1, 1, 16, 16}},
		 {"mean_0.tmp_0", {1, 1, 16, 16}},
		 {"relu_54.tmp_0", {1, 128, 16, 16}},
		 {"max_4.tmp_0", {1, 1, 64, 64}},
		 {"max_5.tmp_0", {1, 1, 64, 64}},
		 {"mean_4.tmp_0", {1, 1, 64, 64}},
		 {"relu_60.tmp_0", {1, 96, 64, 64}},
		 {"mean_2.tmp_0", {1, 1, 32, 32}},
		  {"relu_57.tmp_0", {1, 128, 32, 32}},
	};

	config.SetTRTDynamicShapeInfo(min_input_shape, max_input_shape,
		opt_input_shape);*/

	//创建预测器
	g_predictor = paddle_infer::CreatePredictor(config);

	//定义预处理均值和方差
	g_fmean.push_back(0.5);
	g_fmean.push_back(0.5);
	g_fmean.push_back(0.5);
	g_fstd.push_back(0.5);
	g_fstd.push_back(0.5);
	g_fstd.push_back(0.5);
}
  • 推理predict函数
    预测部分主要分为预处理、图像转tensor、预测、后处理这样几个步骤。其中尤其需要注意取出数据的部分,需要对照模型真实的最终输出来操作。例如,本文的语义分割模型最后的输出数据格式为int64,因此我们使用std::vector out_data这样的变量来提取数据,否则会报错误。
void predict(Mat& org_img, Mat& mask)
{
	//拷贝图像
	int orgWidth = org_img.cols;
	int orgHeight = org_img.rows;
	Mat img = org_img.clone();

	//预处理
	cvtColor(img, img, cv::COLOR_BGR2RGB);
	resize(img, img, cv::Size(target_width, target_height), 0, 0, cv::INTER_LINEAR);
	int real_buffer_size = 3 * target_width * target_height;
	std::vector<float> input_buffer;
	input_buffer.resize(real_buffer_size);
	normalize(img, input_buffer.data(), g_fmean, g_fstd);

	//转tensor
	auto input_names = g_predictor->GetInputNames();
	auto im_tensor = g_predictor->GetInputHandle(input_names[0]);
	im_tensor->Reshape({ 1, 3, target_height, target_width });
	im_tensor->CopyFromCpu(input_buffer.data());

	//执行预测
	g_predictor->Run();

	//取出预测结果
	auto output_names = g_predictor->GetOutputNames();
	auto output_t = g_predictor->GetOutputHandle(output_names[0]);
	std::vector<int> output_shape = output_t->shape();
	int out_num = 1;
	std::cout << "size of outputs[" << 0 << "]: (";
	for (int j = 0; j < output_shape.size(); ++j) {
		out_num *= output_shape[j];
		std::cout << output_shape[j] << ",";
	}
	std::cout << ")" << std::endl;

	std::vector<int64> out_data;
	out_data.resize(out_num);
	output_t->CopyToCpu(out_data.data());

	//后处理获得掩码图
	std::vector<uint8_t> out_data_u8(out_num);
	for (int i = 0; i < out_num; i++) {
		out_data_u8[i] = static_cast<uint8_t>(out_data[i]);
	}
	cv::Mat out_gray_img(output_shape[1], output_shape[2], CV_8UC1, out_data_u8.data());
	cv::resize(out_gray_img, out_gray_img, Size(orgWidth, orgHeight));
	cv::Mat out_eq_img;
	cv::equalizeHist(out_gray_img, mask);

	//结束清理
	img.release();
	out_eq_img.release();
	std::vector<int64>(out_data).swap(out_data);
	std::vector<uint8_t>(out_data_u8).swap(out_data_u8);
	std::vector<float>(input_buffer).swap(input_buffer);
	std::vector<int>(output_shape).swap(output_shape);
	im_tensor.release();
	output_t.release();
}

那么怎么查看我们模型的输出呢?
我们可以使用visualdl工具来查看具体的模型结构,命令如下(需要提前安装好visualdl):

visuladl --model model.pdmodel
  • 归一化normalize函数
    在前面predict函数中我们使用了normalize来预处理数据,这里主要做两件事,一是将数据从[0,255]转换到[0,1]之间,然后除以均值和方差,另外,还需要将图像按照HWC的排列方式转换为CHW的方式。
void normalize(cv::Mat& im, float* data, std::vector<float>& fmean,
	std::vector<float>& fstd) {
	int rh = im.rows;
	int rw = im.cols;
	int rc = im.channels();
	double normf = static_cast<double>(1.0) / 255.0;
#pragma omp parallel for
	for (int h = 0; h < rh; ++h) {
		const uchar* ptr = im.ptr<uchar>(h);
		int im_index = 0;
		for (int w = 0; w < rw; ++w) {
			for (int c = 0; c < rc; ++c) {
				int top_index = (c * rh + h) * rw + w;
				float pixel = static_cast<float>(ptr[im_index++]);
				pixel = (pixel * normf - fmean[c]) / fstd[c];
				data[top_index] = pixel;
			}
		}
	}
}
  • tensorrt加速问题
    在使用tensorrt加速时,经常会有模型因为动态尺寸的问题导致不能使用tensorrt。解决办法其实也很简单,只需要通过config.SetTRTDynamicShapeInfo来设置动态变量的尺寸即可。但是这里有2个核心的问题。
    1、我们怎么知道是哪些动态变量需要设置呢?
    2、这些动态变量设置多大合适呢?
    第1个问题其实不难,因为开启tensorrt之后每次推理一旦报错都会有提示,提示哪些变量目前还没有设置为动态尺寸,我们只需要记住这些变量即可。
    第2个问题有点麻烦,我们需要使用visualdl工具来查看模型结构,然后根据动态变量名称查找到指定的节点,然后再推理当前连接线的输入尺寸(上一个节点的输出尺寸)对应的形状,这里往往需要根据邻接的局部网络进行分析。

例如对于变量bilinear_interp_v2_4.tmp_0来说(运行后tensorrt提示该变量需要设置动态尺寸),我们可以通过visualdl来找到它的模型结构,如下图所示:
在这里插入图片描述
可以看到这时候这根线当前提示是1x128x?x?,说明有两个维度不清楚,因此我们需要通过局部去推理,在下面的cacat输出是1x4x32x32,因此,我们可以认为bilinear_interp_v2_4.tmp_0的最佳输入是1x128x32x32。其他有问题的节点也按照这种方式推理即可。

4.5 完整推理

//导入系统库
#include <iostream> 

//导入opencv库
#include <opencv2/opencv.hpp> 
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp> 

//导入paddle库
#include <paddle_inference_api.h>

using namespace cv;

//定义全局变量
std::shared_ptr<paddle_infer::Predictor> g_predictor;

int target_width = 512;
int target_height = 512;

std::vector<float> g_fmean;
std::vector<float> g_fstd;


void init() {
	// 创建默认配置对象
	paddle_infer::Config config;
	config.SetModel("model/model.pdmodel", "model/model.pdiparams");
	config.EnableUseGpu(100, 0);
	config.EnableMemoryOptim();

	//开启tensorrt加速
	/*config.EnableTensorRtEngine(1 << 20, 1, 10,paddle_infer::PrecisionType::kFloat32, true, false);

	std::map<std::string, std::vector<int>> min_input_shape = {
			  {"x", {1, 3, 512, 512}},
			  {"bilinear_interp_v2_4.tmp_0", {1, 128, 32, 32}},	
			  {"bilinear_interp_v2_5.tmp_0", {1, 96, 64, 64}},
			  {"max_2.tmp_0", {1, 1, 32, 32}},	
			  {"bilinear_interp_v2_3.tmp_0", {1, 128, 16, 16}},
			  {"max_0.tmp_0", {1, 1, 16, 16}},
			  {"max_3.tmp_0", {1, 1, 32, 32}},
			  {"max_1.tmp_0", {1, 1, 16, 16}},
			  {"mean_0.tmp_0", {1, 1, 16, 16}},
			  {"relu_54.tmp_0", {1, 128, 16, 16}}, 
			  {"relu_60.tmp_0", {1, 96, 64, 64}},
			  {"max_4.tmp_0", {1, 1, 64, 64}},
			  {"max_5.tmp_0", {1, 1, 64, 64}},
			  {"mean_4.tmp_0", {1, 1, 64, 64}},
			  {"mean_2.tmp_0", {1, 1, 32, 32}},
			   {"relu_57.tmp_0", {1, 128, 32, 32}},
	};
	std::map<std::string, std::vector<int>> max_input_shape = {
		{"x", {1, 3, 512, 512}},
		{"bilinear_interp_v2_4.tmp_0", {1, 128, 32, 32}},
		{"bilinear_interp_v2_5.tmp_0", {1, 96, 64, 64}},
		 {"max_2.tmp_0", {1, 1, 32, 32}},
		 {"bilinear_interp_v2_3.tmp_0", {1, 128, 16, 16}},
		 {"max_0.tmp_0", {1, 1, 16, 16}},
		 {"max_3.tmp_0", {1, 1, 32, 32}},
		 {"max_1.tmp_0", {1, 1, 16, 16}},
		 {"mean_0.tmp_0", {1, 1, 16, 16}},
		 {"relu_54.tmp_0", {1, 128, 16, 16}},
		 {"max_4.tmp_0", {1, 1, 64, 64}},
		 {"max_5.tmp_0", {1, 1, 64, 64}},
		 {"mean_4.tmp_0", {1, 1, 64, 64}},
		 {"relu_60.tmp_0", {1, 96, 64, 64}},
		 {"mean_2.tmp_0", {1, 1, 32, 32}},
		 {"relu_57.tmp_0", {1, 128, 32, 32}},
	};
	std::map<std::string, std::vector<int>> opt_input_shape = {
		{"x", {1, 3, 512, 512}},
		{"bilinear_interp_v2_4.tmp_0", {1, 128, 32, 32}},
		{"bilinear_interp_v2_5.tmp_0", {1, 96, 64, 64}},
		 {"max_2.tmp_0", {1, 1, 32, 32}},
		 {"bilinear_interp_v2_3.tmp_0", {1, 128, 16, 16}},
		 {"max_0.tmp_0", {1, 1, 16, 16}},
		 {"max_3.tmp_0", {1, 1, 32, 32}},
		 {"max_1.tmp_0", {1, 1, 16, 16}},
		 {"mean_0.tmp_0", {1, 1, 16, 16}},
		 {"relu_54.tmp_0", {1, 128, 16, 16}},
		 {"max_4.tmp_0", {1, 1, 64, 64}},
		 {"max_5.tmp_0", {1, 1, 64, 64}},
		 {"mean_4.tmp_0", {1, 1, 64, 64}},
		 {"relu_60.tmp_0", {1, 96, 64, 64}},
		 {"mean_2.tmp_0", {1, 1, 32, 32}},
		  {"relu_57.tmp_0", {1, 128, 32, 32}},
	};

	config.SetTRTDynamicShapeInfo(min_input_shape, max_input_shape,
		opt_input_shape);*/

	//创建预测器
	g_predictor = paddle_infer::CreatePredictor(config);

	//定义预处理均值和方差
	g_fmean.push_back(0.5);
	g_fmean.push_back(0.5);
	g_fmean.push_back(0.5);
	g_fstd.push_back(0.5);
	g_fstd.push_back(0.5);
	g_fstd.push_back(0.5);
}

void normalize(cv::Mat& im, float* data, std::vector<float>& fmean,
	std::vector<float>& fstd) {
	int rh = im.rows;
	int rw = im.cols;
	int rc = im.channels();
	double normf = static_cast<double>(1.0) / 255.0;
#pragma omp parallel for
	for (int h = 0; h < rh; ++h) {
		const uchar* ptr = im.ptr<uchar>(h);
		int im_index = 0;
		for (int w = 0; w < rw; ++w) {
			for (int c = 0; c < rc; ++c) {
				int top_index = (c * rh + h) * rw + w;
				float pixel = static_cast<float>(ptr[im_index++]);
				pixel = (pixel * normf - fmean[c]) / fstd[c];
				data[top_index] = pixel;
			}
		}
	}
}

void predict(Mat& org_img, Mat& mask)
{
	//拷贝图像
	int orgWidth = org_img.cols;
	int orgHeight = org_img.rows;
	Mat img = org_img.clone();

	//预处理
	cvtColor(img, img, cv::COLOR_BGR2RGB);
	resize(img, img, cv::Size(target_width, target_height), 0, 0, cv::INTER_LINEAR);
	int real_buffer_size = 3 * target_width * target_height;
	std::vector<float> input_buffer;
	input_buffer.resize(real_buffer_size);
	normalize(img, input_buffer.data(), g_fmean, g_fstd);

	//转tensor
	auto input_names = g_predictor->GetInputNames();
	auto im_tensor = g_predictor->GetInputHandle(input_names[0]);
	im_tensor->Reshape({ 1, 3, target_height, target_width });
	im_tensor->CopyFromCpu(input_buffer.data());

	//执行预测
	g_predictor->Run();

	//取出预测结果
	auto output_names = g_predictor->GetOutputNames();
	auto output_t = g_predictor->GetOutputHandle(output_names[0]);
	std::vector<int> output_shape = output_t->shape();
	int out_num = 1;
	std::cout << "size of outputs[" << 0 << "]: (";
	for (int j = 0; j < output_shape.size(); ++j) {
		out_num *= output_shape[j];
		std::cout << output_shape[j] << ",";
	}
	std::cout << ")" << std::endl;

	std::vector<int64> out_data;
	out_data.resize(out_num);
	output_t->CopyToCpu(out_data.data());

	//后处理获得掩码图
	std::vector<uint8_t> out_data_u8(out_num);
	for (int i = 0; i < out_num; i++) {
		out_data_u8[i] = static_cast<uint8_t>(out_data[i]);
	}
	cv::Mat out_gray_img(output_shape[1], output_shape[2], CV_8UC1, out_data_u8.data());
	cv::resize(out_gray_img, out_gray_img, Size(orgWidth, orgHeight));
	cv::Mat out_eq_img;
	cv::equalizeHist(out_gray_img, mask);

	//结束清理
	img.release();
	out_eq_img.release();
	std::vector<int64>(out_data).swap(out_data);
	std::vector<uint8_t>(out_data_u8).swap(out_data_u8);
	std::vector<float>(input_buffer).swap(input_buffer);
	std::vector<int>(output_shape).swap(output_shape);
	im_tensor.release();
	output_t.release();

}

int main()
{
	//初始化环境
	init();
	
	//加载图像和预处理
	Mat img = imread("test.png",cv::IMREAD_COLOR);
	Mat mask = Mat(img.rows, img.cols, CV_8UC1);

	//开始推理
	predict(img, mask);

	//保存掩码结果
	imwrite("result.jpg", mask);
	std::cout << "处理完成" << std::endl;

	return 0;
}

最终输出1张单通道的灰度图,如下图所示(推理前后):
在这里插入图片描述
输出结果和python下预测是一致的。

五、MFC工程调用

在工业应用领域,目前很多工控机程序是采用MFC和C#开发的,本节内容重点讲解如何在MFC程序中调用Paddle Inference。主要步骤分为两步:

    1. 制作基于C++的dll;
    1. MFC中调用dll实现推理;

5.1 基于C++的dll制作

在第四节,我们实现了基于C++控制台程序的Paddle Inference调用。我们本小节继续在这个demo上进行修改,首先修改这个项目的配置输出,修改为动态库(.dll)形式,如下图所示:
在这里插入图片描述
然后我们在当前项目中添加一个头文件,名为imagetool.h,在这个头文件里面我们来定义两个基本的dll接口,实现初始化环境和推理,完整内容如下:

#pragma once

#ifndef IMAGE_API
#define IMAGE_API

extern "C"
{
	// 初始化
	__declspec(dllexport) int EnvInit();

	// 图像推理
	__declspec(dllexport) int ImageProcess(char* pImgIn, char* pImgOut,int height,int width);
}
#endif

在定义推理接口ImageProcess的过程中,为了从拓展性考虑,我们对输入和输出采用的是char*格式,也就是通过原始图像数据内存指针来传递数据。

接下来我们重新修改PaddleDemo.cpp文件,在头部引入刚定义的imagetool.h文件:

#include "imagetool.h"

然后注释掉原文件的main函数,然后添加对应的dll接口函数,代码如下:

//初始化深度学习环境
int EnvInit()
{
	try
	{
		// 创建默认配置对象
		paddle_infer::Config config;
		config.SetModel("model/model.pdmodel", "model/model.pdiparams");
		config.EnableUseGpu(100, 0);
		config.EnableMemoryOptim();

		//开启tensorrt加速
		/*config.EnableTensorRtEngine(1 << 20, 1, 10,paddle_infer::PrecisionType::kFloat32, true, false);

		std::map<std::string, std::vector<int>> min_input_shape = {
				  {"x", {1, 3, 512, 512}},
				  {"bilinear_interp_v2_4.tmp_0", {1, 128, 32, 32}},
				  {"bilinear_interp_v2_5.tmp_0", {1, 96, 64, 64}},
				  {"max_2.tmp_0", {1, 1, 32, 32}},
				  {"bilinear_interp_v2_3.tmp_0", {1, 128, 16, 16}},
				  {"max_0.tmp_0", {1, 1, 16, 16}},
				  {"max_3.tmp_0", {1, 1, 32, 32}},
				  {"max_1.tmp_0", {1, 1, 16, 16}},
				  {"mean_0.tmp_0", {1, 1, 16, 16}},
				  {"relu_54.tmp_0", {1, 128, 16, 16}},
				  {"relu_60.tmp_0", {1, 96, 64, 64}},
				  {"max_4.tmp_0", {1, 1, 64, 64}},
				  {"max_5.tmp_0", {1, 1, 64, 64}},
				  {"mean_4.tmp_0", {1, 1, 64, 64}},
				  {"mean_2.tmp_0", {1, 1, 32, 32}},
				   {"relu_57.tmp_0", {1, 128, 32, 32}},
		};
		std::map<std::string, std::vector<int>> max_input_shape = {
			{"x", {1, 3, 512, 512}},
			{"bilinear_interp_v2_4.tmp_0", {1, 128, 32, 32}},
			{"bilinear_interp_v2_5.tmp_0", {1, 96, 64, 64}},
			 {"max_2.tmp_0", {1, 1, 32, 32}},
			 {"bilinear_interp_v2_3.tmp_0", {1, 128, 16, 16}},
			 {"max_0.tmp_0", {1, 1, 16, 16}},
			 {"max_3.tmp_0", {1, 1, 32, 32}},
			 {"max_1.tmp_0", {1, 1, 16, 16}},
			 {"mean_0.tmp_0", {1, 1, 16, 16}},
			 {"relu_54.tmp_0", {1, 128, 16, 16}},
			 {"max_4.tmp_0", {1, 1, 64, 64}},
			 {"max_5.tmp_0", {1, 1, 64, 64}},
			 {"mean_4.tmp_0", {1, 1, 64, 64}},
			 {"relu_60.tmp_0", {1, 96, 64, 64}},
			 {"mean_2.tmp_0", {1, 1, 32, 32}},
			 {"relu_57.tmp_0", {1, 128, 32, 32}},
		};
		std::map<std::string, std::vector<int>> opt_input_shape = {
			{"x", {1, 3, 512, 512}},
			{"bilinear_interp_v2_4.tmp_0", {1, 128, 32, 32}},
			{"bilinear_interp_v2_5.tmp_0", {1, 96, 64, 64}},
			 {"max_2.tmp_0", {1, 1, 32, 32}},
			 {"bilinear_interp_v2_3.tmp_0", {1, 128, 16, 16}},
			 {"max_0.tmp_0", {1, 1, 16, 16}},
			 {"max_3.tmp_0", {1, 1, 32, 32}},
			 {"max_1.tmp_0", {1, 1, 16, 16}},
			 {"mean_0.tmp_0", {1, 1, 16, 16}},
			 {"relu_54.tmp_0", {1, 128, 16, 16}},
			 {"max_4.tmp_0", {1, 1, 64, 64}},
			 {"max_5.tmp_0", {1, 1, 64, 64}},
			 {"mean_4.tmp_0", {1, 1, 64, 64}},
			 {"relu_60.tmp_0", {1, 96, 64, 64}},
			 {"mean_2.tmp_0", {1, 1, 32, 32}},
			  {"relu_57.tmp_0", {1, 128, 32, 32}},
		};

		config.SetTRTDynamicShapeInfo(min_input_shape, max_input_shape,
			opt_input_shape);*/

			//创建预测器
		g_predictor = paddle_infer::CreatePredictor(config);

		//定义预处理均值和方差
		g_fmean.push_back(0.5);
		g_fmean.push_back(0.5);
		g_fmean.push_back(0.5);
		g_fstd.push_back(0.5);
		g_fstd.push_back(0.5);
		g_fstd.push_back(0.5);
		return 1;
	}
	catch (...)        // 捕获所有异常
	{
		return -1;
	}	
}

// 图像推理
int ImageProcess(char* pImgIn, char* pImgOut, int height, int width)
{
	Mat img = Mat(height, width, CV_8UC3);
	memcpy(img.data,pImgIn,height*width*3);
	Mat mask = Mat(height, width, CV_8UC1);

	//开始推理
	predict(img, mask);

	//返回
	memcpy(pImgOut, mask.data, height * width);

	//释放
	img.release();
	mask.release();

	return 1;
}

到这里dll工程就修改完了,重新生成工程,在x64/Release目录下会生成对应的dll库文件,包括:

PaddleDemo.lib
PaddleDemo.dll

这两个文件就是我们生成出的深度学习推理文件,后面我们在MFC工程中只需要配置这两个文件即可,不需要再配置Paddle Inference了。

5.2 MFC工程中调用

首先新建一个MFC对话框程序(注意必须创建X64 Release工程)。然后调整整个的资源对话框如下所示:
在这里插入图片描述
具体包括2个picture控件(ID号分别是IDC_PIC_IN和IDC_PIC_OUT)和2个按钮。为了能够在这个MFC程序中加载和使用图像,我们一样使用opencv这个库来完成,具体配置方法与4.2节相同。这里主要牵扯到MFC中图像控件的图像显示问题。

我们下面给出关键的图像选择和显示的代码(熟悉MFC的读者可以自行尝试,代码实现是比较简单的)。
选择图像代码:

//选择图片
void CMFCDemoDlg::OnBnClickedButtonChoose()
{
	// TODO: 在此添加控件通知处理程序代码
	CFileDialog dlg(TRUE, _T("*.png"),
		NULL,
		OFN_ALLOWMULTISELECT | OFN_HIDEREADONLY | OFN_FILEMUSTEXIST,
		_T("image Files(*.png;*.png)|*.png;*.png|All Files (*.*)|*.*||"),
		NULL);

	//打开文件对话框的标题名
	dlg.m_ofn.lpstrTitle = _T("选择图像 ");

	if (dlg.DoModal() != IDOK)
		return;

	//读取图片
	CString mPath = dlg.GetPathName();
	m_imgIn.release();
	m_imgIn = imread(mPath.GetBuffer(0), IMREAD_COLOR);

	m_imgOut.release();
	m_imgOut = Mat(m_imgIn.rows, m_imgIn.cols, CV_8UC3, Scalar(255, 255, 255));

	//获取图片控件矩形框
	CRect rect;
	GetDlgItem(IDC_PIC_IN)->GetClientRect(&rect);
	HWND hwnd1 = GetDlgItem(IDC_PIC_IN)->GetSafeHwnd();
	ShowImage(m_imgIn, hwnd1, rect);

	GetDlgItem(IDC_PIC_OUT)->GetClientRect(&rect);
	HWND hwnd2 = GetDlgItem(IDC_PIC_OUT)->GetSafeHwnd();
	ShowImage(m_imgOut, hwnd2, rect);
}

图像显示代码:

void CMFCDemoDlg::ShowImage(Mat imgSrc, HWND hwnd, CRect &rect) 
{
	//缩放Mat,以适应图片控件大小
	cv::resize(imgSrc, imgSrc, cv::Size(rect.Width(), rect.Height()));

	// 转换格式 ,便于获取BITMAPINFO
	switch (imgSrc.channels())
	{
	case 1:
		cv::cvtColor(imgSrc, imgSrc, COLOR_GRAY2BGRA); // GRAY单通道
		break;
	case 3:
		cv::cvtColor(imgSrc, imgSrc, COLOR_BGR2BGRA);  // BGR三通道
		break;
	default:
		break;
	}

	// 制作bitmapinfo(数据头)
	int pixelBytes = imgSrc.channels() * (imgSrc.depth() + 1);
	BITMAPINFO bitInfo;
	bitInfo.bmiHeader.biBitCount = 8 * pixelBytes;
	bitInfo.bmiHeader.biWidth = imgSrc.cols;
	bitInfo.bmiHeader.biHeight = -imgSrc.rows;   //注意"-"号(正数时倒着绘制)
	bitInfo.bmiHeader.biPlanes = 1;
	bitInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
	bitInfo.bmiHeader.biCompression = BI_RGB;
	bitInfo.bmiHeader.biClrImportant = 0;
	bitInfo.bmiHeader.biClrUsed = 0;
	bitInfo.bmiHeader.biSizeImage = 0;
	bitInfo.bmiHeader.biXPelsPerMeter = 0;
	bitInfo.bmiHeader.biYPelsPerMeter = 0;

	//绘图
	HDC hdc = ::GetDC(hwnd);
	::StretchDIBits(
		hdc,
		0, 0, rect.Width(), rect.Height(),
		0, 0, imgSrc.cols, imgSrc.rows,
		imgSrc.data,
		&bitInfo,
		DIB_RGB_COLORS,
		SRCCOPY
	);
}

其中m_imgIn和m_imgOut是两个opencv的Mat对象,在对话框头文件中作为类内变量定义。
最终效果如下所示:
在这里插入图片描述
我们可以任意选择并切换图像显示。

接下来就是正式的完成深度学习dll加载了。首先将前面生成的PaddleDemo.lib、PaddleDemo.dll以及自行定义的imagetool.h头文件都拷贝到当前项目根目录下,模型文件model文件夹也需要拷贝到当前项目 根目录下,同时将PaddleDemo工程下的所有dll文件也拷贝到当前项目根目录下。另外,将这些文件在当前项目的x64/Release下也拷贝一份,这样能够使用ctrl+f5在VS Code中直接运行了。

完整目录如下所示:
在这里插入图片描述
在当前项目的“属性—链接器—输入—附加依赖项”中,添加PaddleDemo.lib库的引用,如下图所示:
在这里插入图片描述
然后在工程头文件添加自定义头文件引用:

#include "imagetool.h"

接下来我们在对话框初始化函数中编写初始化深度学习环境的代码,如下所示:

BOOL CMFCDemoDlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();
	// IDM_ABOUTBOX 必须在系统命令范围内。
	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
	ASSERT(IDM_ABOUTBOX < 0xF000);

	CMenu* pSysMenu = GetSystemMenu(FALSE);
	if (pSysMenu != nullptr)
	{
		BOOL bNameValid;
		CString strAboutMenu;
		bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
		ASSERT(bNameValid);
		if (!strAboutMenu.IsEmpty())
		{
			pSysMenu->AppendMenu(MF_SEPARATOR);
			pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
		}
	}
	// 设置此对话框的图标。  当应用程序主窗口不是对话框时,框架将自动
	//  执行此操作
	SetIcon(m_hIcon, TRUE);			// 设置大图标
	SetIcon(m_hIcon, FALSE);		// 设置小图标

	// TODO: 在此添加额外的初始化代码
	//初始化深度学习环境
	int result = EnvInit();
	if (result < 0)
	{
		MessageBox("初始化深度学习环境失败");
		return false;
	}
	return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE
}

然后我们可以运行一下程序,如果不报错误说明深度学习模型能够被正确加载。最后我们来完成检测的代码:

//检测
void CMFCDemoDlg::OnBnClickedButtonSeg()
{
	// TODO: 在此添加控件通知处理程序代码
	//推理
	m_imgOut.release();
	m_imgOut = Mat(m_imgIn.rows, m_imgIn.cols, CV_8UC1, Scalar(0));
	int result = ImageProcess((char *)m_imgIn.data,(char *)m_imgOut.data,m_imgIn.rows,m_imgIn.cols);

	//显示结果
	CRect rect;
	GetDlgItem(IDC_PIC_OUT)->GetClientRect(&rect);
	HWND hwnd2 = GetDlgItem(IDC_PIC_OUT)->GetSafeHwnd();
	ShowImage(m_imgOut, hwnd2, rect);
}

上述代码需要注意输入和输出图像的格式,需要与dll文件中的一致,否则会内存奔溃。
最终运行效果如下所示:
在这里插入图片描述
注意到,第一次推理速度是比较慢的,后面就很快了。因此,我们可以在初始化的时候做一下warmup(初始时就跑几次推理),这样后面正式推理时速度就快了。

到这里,MFC调用方法就介绍完了。实际读者使用时需要进一步优化上述代码,需要做一些保护操作,例如图像如果读取不到等等。

六、C#工程调用

本节以winform的C#程序为例子,讲解如何在C#程序中调用Paddle Inference。在C#中调用Paddle Inference与第五节一样,都是通过dll的方式调用。首先使用C++生成适合C#的dll,然后再由C#调用。

6.1基于C++的dll制作

本小节先来制作基于C++的dll。首先在项目属性上右键添加模块,然后添加一个PaddleDemo.def文件。如下图所示:
在这里插入图片描述
然后在文件中申明导出模块,这样C#程序才能准确调用这个dll。
代码如下:

LIBRARY PaddleDemo
EXPORTS EnvInit
EXPORTS ImageProcess

然后我们修改自定义的imagetool.h头文件,内容如下:

#pragma once
#define DLL_API extern "C" _declspec(dllexport)

最后我们修改PaddleDemo.cpp,核心代码如下所示:

// 初始化
DLL_API int EnvInit()
{
	try
	{
		// 创建默认配置对象
		paddle_infer::Config config;
		config.SetModel("model/model.pdmodel", "model/model.pdiparams");
		config.EnableUseGpu(100, 0);
		config.EnableMemoryOptim();

		//开启tensorrt加速
		/*config.EnableTensorRtEngine(1 << 20, 1, 10,paddle_infer::PrecisionType::kFloat32, true, false);

		std::map<std::string, std::vector<int>> min_input_shape = {
				  {"x", {1, 3, 512, 512}},
				  {"bilinear_interp_v2_4.tmp_0", {1, 128, 32, 32}},
				  {"bilinear_interp_v2_5.tmp_0", {1, 96, 64, 64}},
				  {"max_2.tmp_0", {1, 1, 32, 32}},
				  {"bilinear_interp_v2_3.tmp_0", {1, 128, 16, 16}},
				  {"max_0.tmp_0", {1, 1, 16, 16}},
				  {"max_3.tmp_0", {1, 1, 32, 32}},
				  {"max_1.tmp_0", {1, 1, 16, 16}},
				  {"mean_0.tmp_0", {1, 1, 16, 16}},
				  {"relu_54.tmp_0", {1, 128, 16, 16}},
				  {"relu_60.tmp_0", {1, 96, 64, 64}},
				  {"max_4.tmp_0", {1, 1, 64, 64}},
				  {"max_5.tmp_0", {1, 1, 64, 64}},
				  {"mean_4.tmp_0", {1, 1, 64, 64}},
				  {"mean_2.tmp_0", {1, 1, 32, 32}},
				   {"relu_57.tmp_0", {1, 128, 32, 32}},
		};
		std::map<std::string, std::vector<int>> max_input_shape = {
			{"x", {1, 3, 512, 512}},
			{"bilinear_interp_v2_4.tmp_0", {1, 128, 32, 32}},
			{"bilinear_interp_v2_5.tmp_0", {1, 96, 64, 64}},
			 {"max_2.tmp_0", {1, 1, 32, 32}},
			 {"bilinear_interp_v2_3.tmp_0", {1, 128, 16, 16}},
			 {"max_0.tmp_0", {1, 1, 16, 16}},
			 {"max_3.tmp_0", {1, 1, 32, 32}},
			 {"max_1.tmp_0", {1, 1, 16, 16}},
			 {"mean_0.tmp_0", {1, 1, 16, 16}},
			 {"relu_54.tmp_0", {1, 128, 16, 16}},
			 {"max_4.tmp_0", {1, 1, 64, 64}},
			 {"max_5.tmp_0", {1, 1, 64, 64}},
			 {"mean_4.tmp_0", {1, 1, 64, 64}},
			 {"relu_60.tmp_0", {1, 96, 64, 64}},
			 {"mean_2.tmp_0", {1, 1, 32, 32}},
			 {"relu_57.tmp_0", {1, 128, 32, 32}},
		};
		std::map<std::string, std::vector<int>> opt_input_shape = {
			{"x", {1, 3, 512, 512}},
			{"bilinear_interp_v2_4.tmp_0", {1, 128, 32, 32}},
			{"bilinear_interp_v2_5.tmp_0", {1, 96, 64, 64}},
			 {"max_2.tmp_0", {1, 1, 32, 32}},
			 {"bilinear_interp_v2_3.tmp_0", {1, 128, 16, 16}},
			 {"max_0.tmp_0", {1, 1, 16, 16}},
			 {"max_3.tmp_0", {1, 1, 32, 32}},
			 {"max_1.tmp_0", {1, 1, 16, 16}},
			 {"mean_0.tmp_0", {1, 1, 16, 16}},
			 {"relu_54.tmp_0", {1, 128, 16, 16}},
			 {"max_4.tmp_0", {1, 1, 64, 64}},
			 {"max_5.tmp_0", {1, 1, 64, 64}},
			 {"mean_4.tmp_0", {1, 1, 64, 64}},
			 {"relu_60.tmp_0", {1, 96, 64, 64}},
			 {"mean_2.tmp_0", {1, 1, 32, 32}},
			  {"relu_57.tmp_0", {1, 128, 32, 32}},
		};

		config.SetTRTDynamicShapeInfo(min_input_shape, max_input_shape,
			opt_input_shape);*/

			//创建预测器
		g_predictor = paddle_infer::CreatePredictor(config);

		//定义预处理均值和方差
		g_fmean.push_back(0.5);
		g_fmean.push_back(0.5);
		g_fmean.push_back(0.5);
		g_fstd.push_back(0.5);
		g_fstd.push_back(0.5);
		g_fstd.push_back(0.5);
		return 1;
	}
	catch (...)        // 捕获所有异常
	{
		return -1;
	}
}

// 图像推理
DLL_API int ImageProcess(uchar* pDataIn, int width, int height, int stride, uchar* pDataOut, size_t& size)
{
	Mat img = Mat(height,width, CV_8UC3, pDataIn, stride).clone();
	Mat mask = Mat(height, width, CV_8UC1);

	//开始推理
	predict(img, mask);

	// 图像数据导出
	std::vector<uchar> buf;
	cv::imencode(".bmp", mask, buf);

	size = buf.size();
	for (uchar& var : buf)
	{
		*pDataOut = var;
		pDataOut++;
	}

	//释放
	img.release();
	mask.release();
	std::vector<uchar>(buf).swap(buf);

	return 1;
}

这里尤其要注意推理函数ImageProcess的写法,这种写法可以保证将来C#能够通过图像内存地址的方式传递数据。

最后重新生成项目解决方案即可得到对应的PaddleDemo.dll和PaddleDemo.lib。对于C#调用来说,只需要PaddleDemo.dll这个文件即可。

6.2 C#中调用dll

首先我们新建一个基于C#的winform程序,然后生成平台改成Release X64。然后我们将前面生成的PaddleDemo.dll以及所有相关的dll以及存放模型的model文件夹在项目本地和x64/Release文件夹下都拷贝一份。

整个C#工程界面设计如下(2个picturebox控件以及两个按钮控件):
在这里插入图片描述
选择图片相关代码主要实现图片的本地选择和读取显示,代码如下:

        public static Bitmap KiResizeImage(Bitmap bmp, int newW, int newH)
        {
            try
            {
                Bitmap b = new Bitmap(newW, newH);
                Graphics g = Graphics.FromImage(b);

                // 插值算法的质量
                g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                g.DrawImage(bmp, new Rectangle(0, 0, newW, newH), new Rectangle(0, 0, bmp.Width, bmp.Height), GraphicsUnit.Pixel);
                g.Dispose();
                return b;
            }
            catch
            {
                return null;
            }
        }

        //图像对象
        private System.Drawing.Bitmap m_ImgIn;
        private System.Drawing.Bitmap m_ImgOut;
        // 选择图片
        private void button_choose_Click(object sender, EventArgs e)
        {
            OpenFileDialog opnDlg = new OpenFileDialog();
            opnDlg.Filter = "所有图像文件 | *.png;";
            opnDlg.Title = "打开图像文件";
            opnDlg.ShowHelp = true;
            if (opnDlg.ShowDialog() == DialogResult.OK)
            {
                string curFileName = opnDlg.FileName;
                try
                {
                    //读取图片
                    m_ImgIn = (Bitmap)Image.FromFile(curFileName);

                    //显示
                    Bitmap img = KiResizeImage(m_ImgIn, pictureBoxIn.Size.Width, pictureBoxIn.Size.Height);
                    pictureBoxIn.Image = img;
                    pictureBoxOut.Image = null;
                    pictureBoxOut.Refresh();
                }
                catch (Exception exp)
                {
                    MessageBox.Show(exp.Message);
                }
            }
        }

接下来我们在对话框类初始化函数中调用dll 的EnvInit函数实现深度学习模型的读取加载,代码如下:

        [DllImport("PaddleDemo.dll")]
        private extern static int EnvInit();

        [DllImport("PaddleDemo.dll")]
        private extern static int ImageProcess(byte []pDataIn,int width, int height, int stride, ref byte pDataOut, out ulong size);
        public l()
        {
            InitializeComponent();
            int result = EnvInit();
            if (result < 0)
            {
                MessageBox.Show("深度学习环境加载失败");
            }
        }

注意上述代码中我们将dll中的两个接口函数全部引用了进来,这里需要关注这种接口引用方式。

最后我们完成检测按钮的代码:

        private void button_detect_Click(object sender, EventArgs e)
        {
            //输入数据(强转为BGR格式)
            BitmapData imgData = m_ImgIn.LockBits(new Rectangle(0, 0, m_ImgIn.Width, m_ImgIn.Height), ImageLockMode.ReadWrite,
                 PixelFormat.Format24bppRgb);
            int width = imgData.Width;
            int height = imgData.Height;
            int stride = imgData.Stride;
            IntPtr ptr = imgData.Scan0;
            // Declare an array to hold the bytes of the bitmap. 
            int bytesLength = Math.Abs(imgData.Stride) * m_ImgIn.Height;
            //图像的Stride
            byte[] buffer = new byte[bytesLength];
            // Copy the RGB values into the array.
            Marshal.Copy(ptr, buffer, 0, bytesLength);

            //MessageBox.Show(stride.ToString());

            //输出数据
            byte[] ptrData = new byte[1024 * 1024 * 3]; //尽可能大的byte[],一般大于显示的最大图片内存即可
            ulong size = new ulong();  

            //推理
            int result = ImageProcess(buffer, width, height,stride, ref ptrData[0], out size);
            if (result < 0)
            {
                MessageBox.Show("图像优化失败  原因:" + result.ToString());
                return;
            }

            m_ImgOut = (Bitmap)Image.FromStream(new MemoryStream(ptrData, 0, (int)size));

            Bitmap img = KiResizeImage(m_ImgOut, pictureBoxOut.Size.Width, pictureBoxOut.Size.Height);
            pictureBoxOut.Image = img;

            m_ImgIn.UnlockBits(imgData);
        }

最终效果如下所示:
在这里插入图片描述

七、完整代码链接

所有代码均放在了百度网盘上,读者可以自行下载:
链接: https://pan.baidu.com/s/1nIZXe65VXKBTtcOypsLIXA 提取码: avm6

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

一文掌握面向Windows平台的深度学习工控程序开发(使用Paddle Inference部署MFC、C#程序,内含完整代码链接) 的相关文章

  • Python大佬手把手教你利用wxPython模块编写界面程序

    wxPython是一个开发桌面端图形界面的跨平台函数库 xff0c 开发语言为Python xff0c 它是基于C 43 43 的函数库wxWidgets的封装 很多人学习python xff0c 不知道从何学起 很多人学习python x
  • 什么是栈,栈存储结构详情

    什么是栈 xff0c 栈存储结构详情 同顺序表和链表一样 xff0c 栈也是用来存储逻辑关系为一对一数据的线性存储结构 xff0c 如图所示 从图1我们看到 xff0c 栈存储结构与之前学的线性存储有所差异 xff0c 这源于栈对数存和取的
  • C++cmath数学常用库中的代码介绍

    今天小编来分享一波C 43 43 cmath数学常用库中的常用代码 首先 xff0c 这些所有的代码都来自cmath库 xff0c 所以必须先引用cmath头文件 xff0c 即 xff1a include lt cmath gt 1 绝对
  • vue-cli-service Axios 持续返还401

    后端 xff1a 在登录超时或没有登录的情况 xff0c 所有请求都会拒绝并且返还HTTP状态码401 前端 xff1a 前端将认证的Token放到loadStorage中 xff0c 在认证失效时候清空 在接到HTTP状态码为401时 x
  • 载波相位测量

    1 简述GPS载波相位测量的基本原理 载波相位测量的观测量是GPS接收机所接收的卫星载波信号与接收机本振参考信号的相位差 利用接收机测定载波相位观测值 xff0c 经基线向量解算以获得两个同步观测站之间的基线向量坐标差 2 在高精度GPS测
  • PJLib开发杂谈

    去年公司搞了一个智慧乡村的项目 xff0c 其中涉及到视频监控的问题 电信提供的解决方案 xff0c 我们的平台只需要跟电信对接HLS流就可以了 这就有一个问题 xff0c 原来公司安装的监控无法接入到自己的平台 xff0c 好尴尬啊 于是
  • 开发GB28181监控平台前期准备总结

    首先得准备PJLIB的编译 xff0c 这个搜一下 xff0c 下载以后它是有VS的工程文件的 xff0c 所以编译很方便 得到这个库以后 xff0c 就可以编写SIP服务程序了 xff0c 服务程序可以验证GB28181的相关通讯流程 x
  • esp8266 丢失固件 丢失程序问题

    1 首先esp8266 丢失固件 丢失程序问题已经解决 2 解决方法 我们制作了一个固件保护主板 xff0c 提供2种供电接口 xff0c 支持5v稳压 串口电平保护 xff0c 固件保护 xff0c 反电动势保护 xff0c 支持复位按键
  • printf重定向

    1 printf与fputc 对于 printf 函数相信大家都不陌生 xff0c 第一个C语言程序就是使用 printf 函数在屏幕上的控制台打印出Hello World xff0c 之后使用 printf 函数输出各种类型的数据 xff
  • ESP32_BLUFI代码移植过程遇到的问题

    1 先是运行esp32官方给的例程 xff0c 出现了错误报错如下 xff1a esp image Image length 1053648 doesn t fit in partition length 1048576 boot Fact
  • Java 中的 Iterator 迭代器详解

    x1f366 Iterator 接口 在程序开发中 xff0c 经常需要遍历集合中的所有元素 针对这种需求 xff0c JDK 专门提供了一个接口 java util Iterator Iterator 接口也是 Java 集合中的一员 x
  • 三.【NodeJs入门学习】POST接口

    上一节我们学习了get接口 xff0c 这一节我们自己来写一下post接口 1 复习一下 先复习一下上一节中get请求的步骤 上图是在入口app js中处理get请求 xff0c 先拿到请求的url xff0c 然后设置了一个函数handl
  • 多进程和多线程比较

    原文 xff1a http blog csdn net lishenglong666 article details 8557215 很详细 对比维度 多进程 多线程 总结 数据共享 同步 数据共享复杂 xff0c 需要用IPC xff1b
  • C++ 之头文件声明定义

    最近在学习 c 43 43 在编译与链接过程中遇到了一些定义与声明的问题 经过多处查阅资料 基本解惑 现记录与此 希望让后面人少走些弯路 C 43 43 的头文件应该用什么扩展名 目前业界的常用格式如下 implementation fil
  • arduino修改串口缓冲区大小的三种办法

    由于SoftwareSerial h默认只接收64字节串行缓冲区 xff0c Arduino会将之后接收到的数据丢弃 xff0c 不满足业务需求 以下三种方法是笔者参考网上各种资料总结出来 xff0c 对于WEMOS D1 R2 xff0c
  • C语言调用libcurl的一个简单例子

    首先我们创建一个php页面 xff1a lt meta http equiv 61 span class hljs string 34 Content Type 34 span content 61 span class hljs stri
  • 【C++】类构造函数、析构函数的调用顺序「完整版」

    一 全局变量 静态变量和局部变量 全局变量在程序开始时调用构造函数 在程序结束时调用析构函数 静态变量在所在函数第一次被调用时调用构造函数 在程序结束时调用析构函数 xff0c 只调用一次 局部变量在所在的代码段被执行时调用构造函数 xff
  • linux下使用shell发送http请求

    本文主要介绍如何在linux下使用shell发送http请求 一 curl 1 get请求 curl命令默认下就是使用get方式发送http请求 curl www span class hljs preprocessor baidu spa
  • 【STL真好用】1057 Stack C++(30)

    1057 Stack 30 分 Stack is one of the most fundamental data structures which is based on the principle of Last In First Ou
  • C++学习之头文件引用

    目录结构如下 test h的定义如下 xff1a ifndef TEST H define TEST H include lt vector gt include lt string gt using namespace std class

随机推荐

  • checksum 算法

    说明 checksum xff1a 总和检验码 xff0c 校验和 xff0c 可以理解为check xff08 校验 xff09 xff0c sum xff08 和 xff09 在数据处理和通信领域 xff0c 通过一定算法对传输的数据进
  • 解决cannot open shared object file: No such file or directory

    一 linux下调用动态库 so文件时提示 xff1a cannot open shared object file No such file or directory 解决办法 xff1a 1 此时ldd xxx查看依赖缺少哪些库 lib
  • cmake 使用(六)

    本文是 cmake 使用的第六篇 主要介绍如何设置编译器优化标志 上一篇的链接为 xff1a https blog csdn net QCZL CC article details 119825737 xff0c 主要介绍如何将自己的软件安
  • 8086寄存器介绍

    8086 有14个16位寄存器 xff0c 这14个寄存器按其用途可分为 1 通用寄存器 2 指令指针 3 标志寄存器和 4 段寄存器等4类 1 通用寄存器有8个 又可以分成2组 一组是数据寄存器 4个 另一组是指针寄存器及变址寄存器 4个
  • C++常用操作符:: -> . (例子详解)

    C 43 43 提供了三种访问类或者类对象的操作符 xff0c 他们是 双冒号 点 箭头 gt 这三种操作符有着各自的使用场景和定义 双冒号 A B 表示作用域运算符 A一定是一个类的名称或命名空间的名称 仅仅用于当B是A类 A命名空间的一
  • STM32中断优先级的分配以及中断原则

    STM32d的中断优先级由NVIC IPRx寄存器来配置 xff0c IPR的宽度为8bit所以原则上每个中断可配置的优先级为0 255 xff0c 数值越小优先级越高 xff0c 但对于大部分的 Cortex M3芯片都会精简设计 xff
  • 晶体管的结构、类型和三种组态

    晶体管有两大类型 双极型晶体管 BJT 和场效应管 FET 双极型晶体管又称为半导体三极管 晶体三极管 xff0c 简称晶体管 它由两个PN结组合而成 xff0c 有两种载流子参与导电是一种电流控制电流源器件 场效应管仅有一种载流子参与导电
  • STM32单片机基础09——重定向printf函数到串口输出的多种方法

    本文详细的介绍了如何重定向printf输出到串口输出的多种方法 xff0c 包括调用MDK微库 xff08 MicroLib xff09 的方法 xff0c 调用标准库的方法 xff0c 以及适用于 GNUC 系列编译器的方法 1 prin
  • STM32直流减速电机控制篇(一)PWM调速

    直流电机原理 下面是分析直流电机的物理模型图 其中 xff0c 固定部分有磁铁 xff0c 这里称作主磁极 xff1b 固定部分还有电刷 转动部分有环形铁芯和绕在环形铁芯上的绕组 直流电机的转动原理我就不再赘述 xff0c 比较简单易懂 直
  • STM32直流减速电机控制篇(二)编码器测速原理

    编码器 编码器是一种将角位移或者角速度转换成一连串电数字脉冲的旋转式传感器 xff0c 我们可以通过编码器测量到底位移或者速度信息 编码器从输出数据类型上分可以分为增量式编码器和绝对式编码器 从编码器检测原理上来分 xff0c 还可以分为光
  • STM32直流减速电机控制篇(三)编码器测速程序编写

    编程思路 任何一个程序的编写我们都应该先理清楚编程思路 xff0c 通过上一篇讲解的编码器测速原理我们应该知道要想通过编码器得知电机转速我们第一步就应该是捕获A相和B相输出的脉冲 因为电机速度的定义是单位时间内的转数 xff0c 所以第二步
  • GPIO模式

    开漏输出 只能输出低电平 xff0c 不能输出高电
  • 单片机485通信

    1 RS485简介 485 xff08 一般称作 RS485 EIA 485 xff09 是隶属于 OSI 模型物理层的电气特性规定为 2 线 xff0c 半双工 xff0c 多点信的标准 它的电气特性和 RS 232 大不一样 用缆线两端
  • Jetson Xavier NX 镜像制作、烧录及克隆

    以下所有方法仅适用于Jetson Xavier Nx 16G emmc版本 其他版本仅供参考 官方文档下载链接为https developer nvidia com embedded downloads search 61 Develope
  • Postman下载,安装,注册及登录教程

    目录 一 Postman简介 二 Postman的注册 1 首先下载Postman xff0c 进入官网 xff1a Download Postman Get Started for Free 2 安装Postman 3 找到所下载的app
  • 一文掌握fastapi微服务开发

    目录 一 概述 1 1 微服务 1 1 1 微服务的优势 1 1 2 微服务的缺点 1 2 为何使用Python开发微服务 1 3 FastAPI概述 二 开发 2 1 安装FastAPI 2 1 1 安装虚拟环境 2 1 2 创建虚拟环境
  • Windows通过SSH连接虚拟机中的ubuntu系统

    zz windows通过ssh连接虚拟机中的ubuntu步骤 音量 博客园
  • PaddleServing图像语义分割部署实践

    目录 一 任务概述 二 官方示例部署 2 1 安装PaddleServing 2 2 导出静态图模型 2 3 转换为serving模型 2 4 启动服务 2 5 客户端请求 三 基于PipeLine的抠图功能部署 3 1 基于深度学习的抠图
  • C/C++资源大全(各种库、框架等)

    转载 https www cplusplus me 2182 html C 43 43 资源大全 各种库 框架等 目录 隐藏 1 标准库2 框架3 人工智能4 异步事件循环5 音频6 生态学7 压缩8 并发性9 容器10 密码学11 数据库
  • 一文掌握面向Windows平台的深度学习工控程序开发(使用Paddle Inference部署MFC、C#程序,内含完整代码链接)

    目录 一 概述1 1 智能制造和飞桨1 2 Paddle Inference工业级应用部署工具 二 算法训练和导出2 1 任务概述和实现原理2 2 训练和静态模型导出 三 部署环境准备四 Windows下C 43 43 工程编译和运行4 1