基于Windows平台在C++中调用Pytorch模型并实现MFC集成(以MNIST手写体数字识别为例)——附完整代码和数据

本教程将完整的记录使用pytorch从模型训练到模型调用(基于Python),再通过libtorch转成C++调用(基于win32 C++控制台程序),最终集成到MFC程序中来,这样就可以完整的在Windows下走通 AI 算法建模到生产级部署的全部流程。

基本配置环境如下:

Python版本:Python 3.6.1

Pytorch版本:1.2.0

Libtorch:1.3

操作系统:Win10

编译器:VS 2015

1.算法—MNIST手写体数字识别

MNIST是非常有名的手写体数字识别数据集,非常适合用来实战讲解,因此我们也以此作为项目的开始,通过Pytorch建立算法来识别图像中的数字。MNIST由手写体数字的图片和相对应的标签组成,如下图所示:

Pytorch的torchvision库中已经自带了该数据集,可以直接下载和导入。

下面给出完整的训练代码train.py:

import argparse
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import PIL

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x=self.conv1(x)
        x = F.relu(F.max_pool2d(x, 2))
        x=self.conv2(x)
        x = F.relu(F.max_pool2d(self.conv2_drop(x), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)

def train(args, model, device, train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        if batch_idx % args.log_interval == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item()))

def test(args, model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction='sum').item() # sum up batch loss
            pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))

def main():
    # 用于训练的超参数设置
    parser = argparse.ArgumentParser(description='PyTorch MNIST样例')
    parser.add_argument('--batch-size', type=int, default=64,
                        help='input batch size for training (default: 64)')
    parser.add_argument('--test-batch-size', type=int, default=1000,
                        help='input batch size for testing (default: 1000)')
    parser.add_argument('--epochs', type=int, default=10,
                        help='number of epochs to train (default: 10)')
    parser.add_argument('--lr', type=float, default=0.01,
                        help='learning rate (default: 0.01)')
    parser.add_argument('--momentum', type=float, default=0.5,
                        help='SGD momentum (default: 0.5)')
    parser.add_argument('--no-cuda', action='store_true', default=False,
                        help='disables CUDA training')
    parser.add_argument('--seed', type=int, default=1,
                        help='random seed (default: 1)')
    parser.add_argument('--log-interval', type=int, default=10,
                        help='how many batches to wait before logging training status')
    args = parser.parse_args()
    use_cuda = not args.no_cuda and torch.cuda.is_available() # 判断是否使用GPU训练
    print(use_cuda)
    torch.manual_seed(args.seed) # 固定住随机种子,使训练结果可复现

    device = torch.device("cuda" if use_cuda else "cpu")

    kwargs = {'num_workers': 8, 'pin_memory': True} if use_cuda else {}
    # 加载训练数据
    train_loader = torch.utils.data.DataLoader(
        datasets.MNIST('data', train=True, download=True,
                       transform=transforms.Compose([
                           transforms.ToTensor(),
                           transforms.Normalize((0.1307,), (0.3081,))
                       ])),
        batch_size=args.batch_size, shuffle=True, **kwargs)
    #加载测试数据
    test_loader = torch.utils.data.DataLoader(
        datasets.MNIST('data', train=False, transform=transforms.Compose([
                           transforms.ToTensor(),
                           transforms.Normalize((0.1307,), (0.3081,))
                       ])),
        batch_size=args.test_batch_size, shuffle=True, **kwargs)


    model = Net().to(device)
    optimizer = optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum)

    for epoch in range(1, args.epochs + 1):
        train(args, model, device, train_loader, optimizer, epoch)
        test(args, model, device, test_loader)

    torch.save(model, "model.pth")# 保存模型参数


if __name__ == '__main__':
    import time
    start = time.time()
    main()
    end = time.time()
    running_time = end-start
    print('time cost : %.5f 秒' %running_time) 

上述代码采用的模型架构比较简单,只用了2个卷积层和2个全连接层,在我的GTX 1080TI上跑了77秒,检测精度达到99%。CPU上跑了311秒,检测精度98%。训练完成后自动保存当前模型参数为model.pth文件。

接下来使用该模型参数文件进行单张图片预测,完整代码文件test.py:

import torch
import cv2
import torch.nn.functional as F
from train import Net 
from torchvision import datasets, transforms
from PIL import Image
 
if __name__ == '__main__':
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = torch.load('model.pth')  # 加载模型
    model = model.to(device)
    model.eval()  # 把模型转为test模式
 
    img = cv2.imread("img_3.jpg",0)  # 读取要预测的灰度图片
    img = Image.fromarray(img)
    trans = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])
 
    img = trans(img)
    img = img.unsqueeze(0)  # 图片扩展多一维,[batch_size,通道,长,宽],此时batch_size=1
    img = img.to(device)
    output = model(img)
    pred = output.max(1, keepdim=True)[1]
    pred = torch.squeeze(pred)
    print('检测结果为:%d' % (pred.cpu().numpy()))

预测结果如下:

输入图片:             输出:2            预测正确。

到这里已经完成算法建模部分。接下来,需要将这个算法部署到生产级的环境中,也就是我们这里需要的C++环境。一般的生产环境不会采用Python(考虑速度和稳定性等因素),尤其是边缘计算等设备,因此需要在C++中实现模型的调用和推理。

2. C++调用—使用libtorch导出序列化模型

从Pytorch-1.0开始其最瞩目的功能就是生产的大力支持,推出了C++版本的生态端,包括C++前端和C++模型编译工具。对于我们来说,在想要部署深度学习应用的时候,只需要在Python端利用Pytorch进行训练,然后使用torch.jit导出我们训练好的模型,再利用C++端的Pytorch接口读取进行预测即可。由于这种方式是官方推荐和支持的,因此,相比于其它方式这种模式更加稳定可靠。官方已经替我们编译好Windows版本的libtorch,这下就节省了我们编译Pytorch的时间,可以直接拿来使用,只要稍微配置一下就可以在Windows上运行libtorch了。

2.1导出序列化模型(pth转pt)

首先我们需要将刚才的算法模型进行序列化导出,基本原理与测试单张图片类似,输入一个样例图片,然后利用训练好的模型对图片进行一次推理。稍微不同的就是,才推理的过程中使用了torch.jit函数来记录了整个推理的一个路径流,最后由torch.jit来保存这个路径流。完整代码如下:

import torch
import cv2
import torch.nn.functional as F
from train import Net 
from torchvision import datasets, transforms
from PIL import Image
 
if __name__ == '__main__':
    device = torch.device('cpu') #使用cpu进行推理
    model = torch.load('model.pth')  # 加载模型
    model = model.to(device)
    model.eval()  # 把模型转为test模式
 
    img = cv2.imread("img.jpg",0)  # 读取要预测的灰度图片
    img = Image.fromarray(img)
    trans = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])
 
    img = trans(img)
    img = img.unsqueeze(0)  # 图片扩展多一维,[batch_size,通道,长,宽]
    img = img.to(device)
    traced_net = torch.jit.trace(model,img)
    traced_net.save("model.pt")

    print("模型序列化导出成功")

这里需要注意,由于我们后面安装的libtorch是cpu版本的,因此,在序列化过程中只使用cpu进行推理,否则序列化出来的模型后面C++没办法正确读取。

2.2安装opencv

在安装libtorch之前,首先需要安装OpenCV,因为在处理图像类操所时不像python版可以用PIL等额外的库,在C++下面使用OpenCV相对比较方便。下载网址:https://opencv.org/releases.html。在版本选择上推荐opencv4.1.1,因为opencv4.1之后,简化了dll和相关的lib文件,所有的依赖都集成在一个巨大的opencv_world.dll文件中,不需要在C++工程中导入n个lib和dll文件了,只需要1个即可。按照页面提示下载Windows版本即可。

下载完成后双击安装包进行解压,解压到某个固定目录。

下面打开VS2015(或者VS2017、VS2019都可以),新建一个win32控制台应用程序mnist。

在创建项目时去掉预编译头和安全开发生命周期SDL检查复选框:

最后单击“完成”实现项目创建。由于libtorch只能在64位windows上运行,因此我们需要修改项目为release x64,如下图所示:

后面所有的项目配置都按照Release x64来配置,至于调试版的Debug x64可以按照这个教程一样的配置即可。

首先生成一下项目,依次单击“生成”—“生成解决方案”,这样可以在项目下生成一个x64/release文件夹,该文件夹下生成了对应的可执行程序mnist.exe(尽管我们还一行代码没有编写)。

然后我们在前面下载的opencv中目录找到opencv\build\x64\vc14\bin文件夹,将bin文件夹中的opencv_videoio_ffmpeg411_64.dll和opencv_world411.dll(如果是debug版本则复制opencv_world411d.dll)文件到刚才项目生成的x64/release文件夹下面。

接下来在项目中配置Opencv。在vs2015菜单栏中依次选择“项目”——“mnist属性”,然后单击左侧“VC++目录”,添加相关路径,包含目录中添加:

E:\toolplace\opencv4.1.1\opencv\build\include

E:\toolplace\opencv4.1.1\opencv\build\include\opencv2

库目录中加入:

E:\toolplace\opencv4.1.1\opencv\build\x64\vc14\lib

如果是高版本VS,则可以导入vc15文件夹中的lib库。然后在“链接器”->“输入”->“附加依赖项”中添加:

opencv_world411.lib

下面在mnist.cpp中添加一段C++测试代码用于测试opencv的安装是否成功:

#include "stdafx.h"

#include <iostream>  
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp>  

using namespace cv;

int main()
{
	// 读入一张图片  
	Mat img = imread("img.jpg");
	// 创建一个名为 "图片"窗口    
	namedWindow("图片");
	// 在窗口中显示图片   
	imshow("图片", img);
	// 等待6000 ms后窗口自动关闭    
	waitKey(6000);
	return 0;
}

重新生成项目,然后在x64/release目录下放置1张测试图片“img.jpg”,运行生成的软件查看图像显示是否正常。效果如下图所示:

2.3安装libtorch

首先去官网下载libtorch:https://pytorch.org/get-started/locally/。按下面的配置下载(下载release 版本):

下载完成后解压,目录结构如下:

其中,include文件夹包含了我们需要的libtorch头文件;lib文件夹中包含了libtorch的库文件(lib和dll)。与opencv类似,我们需要将头文件和lib文件引入到本项目中。

在项目“VC++目录”的“包含目录”中添加相关路径,包含目录中添加:

E:\toolplace\libtorch\include

库目录中加入:

E:\toolplace\libtorch\lib

然后在“链接器”->“输入”->“附加依赖项”中添加:
       c10.lib
       torch.lib
       libprotobuf.lib
       caffe2_detectron_ops.lib

然后保存配置即可。最后将libtorch包中的c10.dll、caffe2_detectron_ops.dll、caffe2_module_test_dynamic.dll、torch.dll、libiomp5md.dll、libiompstubs5md.dll文件拷贝到我们mnist项目的x64/Release文件夹下面。注意,我们所有第三方库都是使用的release版,如果是新手很可能需要在C++中调试程序,建议新手刚开始全部按照debug版本来配置(包括opencv和libtorch),等debug调通后再配置release版本进行产品发布。

2.4 C++中调用模型进行推理

将前面序列化导出的model.pt文件拷贝到mnist项目的x64/Release文件夹下面,这个文件就是我们最终项目进行推理需要的参数文件。完整的c++代码如下:

#include "stdafx.h"
#undef UNICODE   //这个一定要加,否则会编译错误

#include <torch/script.h> // 引入libtorch头文件
#include <iostream>  
#include <memory>
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui_c.h>
#include <opencv2/highgui/highgui.hpp>  

using namespace cv;

int main()
{
	Mat image;
	image = imread("img.jpg", IMREAD_GRAYSCALE);//读取灰度图

	torch::jit::script::Module module;
	try {
		module = torch::jit::load("model.pt");  //加载模型
	}
	catch (const c10::Error& e) {
		std::cerr << "无法加载model.pt模型\n";
		return -1;
	}

	std::vector<int64_t> sizes = { 1, 1, image.rows, image.cols };  
	at::TensorOptions options(at::ScalarType::Byte);
	at::Tensor tensor_image = torch::from_blob(image.data, at::IntList(sizes), options);//将opencv的图像数据转为Tensor张量数据
	tensor_image = tensor_image.toType(at::kFloat);//转为浮点型张量数据
	at::Tensor result = module.forward({ tensor_image }).toTensor();//推理

	auto max_result = result.max(1, true);
	auto max_index = std::get<1>(max_result).item<float>();
	std::cerr << "检测结果为:";
	std::cout << max_index << std::endl;

	waitKey(6000);
}

重新生成解决方案,然后在x64/Release文件夹下放置测试图片img.jpg,双击运行mnist.exe,程序输出结果如下:

至此,已顺利完成C++的模型调用。

尽管本教程针对手写体数字识别,但是在C++调用的代码中,已经实现好了相关的接口,这样针对其它图像类应用,例如:图像检测、图像分类、图像识别、语义分割皆可以参照本教程实现。

2.5 MFC中集成调用

本教程最终希望实现MFC下调用pytorch模型,即给程序封装一个壳,真正的能够让用户便捷的使用,对于有这方面需求的读者可以参照本节继续阅读下去,如果不需要则可以打道回府(顺便无耻的喊一声:能给个5星好评不啦)。

打开VS2015,创建一个MFC应用程序MnistMFC,创建过程如下所示:

其它部分按照默认配置即可,最后单击“完成”实现项目创建。

首先修改解决方案,改为Release X64,并生成一下项目。然后打开资源管理视图,找到工具箱,添加两个Button ,一个命名为“选择图像”,一个命名为“检测”,然后在工具箱中找到picture control控件,添加到我们的界面,并将其ID号重新命名为 IDC_SHOW 。此时界面如下图所示:

接下来按照2.2和2.3节中的部分为项目配置opencv和libtorch库引用,然后将相关dll文件复制到x64/Release文件夹下面,如下图所示:

打开项目的stdafx.h文件,在头部添加libtorch库的导入(这个非常重要!!!放在其它任何地方都会导致编译出问题):


// stdafx.h : 标准系统包含文件的包含文件,
// 或是经常使用但不常更改的
// 特定于项目的包含文件

#undef UNICODE   //这个一定要加,否则会编译错误
#include <torch/script.h> // 引入libtorch头文件

#pragma once



#ifndef VC_EXTRALEAN
#define VC_EXTRALEAN            // 从 Windows 头中排除极少使用的资料
#endif

#include "targetver.h"

#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS      // 某些 CString 构造函数将是显式的

// 关闭 MFC 对某些常见但经常可放心忽略的警告消息的隐藏
#define _AFX_ALL_WARNINGS

#include <afxwin.h>         // MFC 核心组件和标准组件
#include <afxext.h>         // MFC 扩展


#include <afxdisp.h>        // MFC 自动化类



#ifndef _AFX_NO_OLE_SUPPORT
#include <afxdtctl.h>           // MFC 对 Internet Explorer 4 公共控件的支持
#endif
#ifndef _AFX_NO_AFXCMN_SUPPORT
#include <afxcmn.h>             // MFC 对 Windows 公共控件的支持
#endif // _AFX_NO_AFXCMN_SUPPORT

#include <afxcontrolbars.h>     // 功能区和控件条的 MFC 支持


#ifdef _UNICODE
#if defined _M_IX86
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"")
#elif defined _M_X64
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"")
#else
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
#endif
#endif


然后,依次单击菜单“项目”—“MnistMFC 属性”—“配置属性”—“C/C++”—“常规”,将SDL检查改为“否”。配置完成后重新生成解决方案查看是否能够正常编译通过,如果不行说明前面的配置有问题,请仔细核对本教程步骤再试。

在CMnistMFCDlg.h中添加opencv库的导入:

#include <iostream>  
#include <memory>
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui_c.h>
#include <opencv2/highgui/highgui.hpp>  

using namespace cv;

重新生成一下项目。至此,已完成两个库的配置和导入。

下面开始编写逻辑实现代码。首先将项目字符集从“使用Unicode字符集”改为“未设置”,这样就可以比较方便的处理字符串数据转换。然后为CMnistMFCDlg类添加两个类内变量:

public:
	Mat m_img;//图像
	BITMAPINFO *m_imgInfo;//图像颜色表信息

接下来为按钮添加响应函数,首先是“选择图像”对应的响应函数:

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

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

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

	CString mPath = dlg.GetPathName();
	m_img = imread(mPath.GetBuffer(0), IMREAD_GRAYSCALE);//读取灰度图

	//修改颜色表等信息
	m_imgInfo = (BITMAPINFO*) new BYTE[sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD)];
	//颜色表赋值
	for (int i(0); i < 256; ++i)
	{
		m_imgInfo->bmiColors[i].rgbBlue = i;
		m_imgInfo->bmiColors[i].rgbGreen = i;
		m_imgInfo->bmiColors[i].rgbRed = i;
		m_imgInfo->bmiColors[i].rgbReserved = 0;
	}
	//头文件信息(注意由实际显示情况可得出图像原点显示在空间左下角)
	m_imgInfo->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
	m_imgInfo->bmiHeader.biBitCount = 8 * m_img.channels();
	m_imgInfo->bmiHeader.biHeight = -m_img.rows;
	m_imgInfo->bmiHeader.biWidth = m_img.cols;
	m_imgInfo->bmiHeader.biPlanes = 1;
	m_imgInfo->bmiHeader.biCompression = BI_RGB;
	m_imgInfo->bmiHeader.biSizeImage = m_img.channels() * m_img.cols * m_img.rows;
	m_imgInfo->bmiHeader.biXPelsPerMeter = 0;
	m_imgInfo->bmiHeader.biYPelsPerMeter = 0;
	m_imgInfo->bmiHeader.biClrUsed = 0;
	m_imgInfo->bmiHeader.biClrImportant = 0;

	//开始绘图
	CDC *pDC;
	pDC = GetDlgItem(IDC_SHOW)->GetDC();
	CRect imgCtrlRect;
	GetDlgItem(IDC_SHOW)->GetClientRect(&imgCtrlRect);

	pDC->SetStretchBltMode(COLORONCOLOR);
	::StretchDIBits(pDC->GetSafeHdc(),
		0, 0,
		imgCtrlRect.Width(), imgCtrlRect.Height(),
		0, 0,
		m_img.cols, m_img.rows,
		m_img.data,
		m_imgInfo,
		DIB_RGB_COLORS,
		SRCCOPY);

	ReleaseDC(pDC);	
}

根据上述代码就可以打开任意单通道jpg文件并显示。接下来添加第二个响应函数的代码,该代码中需要调用pytorch模型进行预测并显示结果。将前面训练好的model.pt文件拷贝到本项目X64 Release文件夹下面,然后编辑代码如下:

void CMnistMFCDlg::OnBnClickedButtonDetect()
{
	// TODO: 在此添加控件通知处理程序代码
	torch::jit::script::Module module;
	try {
		module = torch::jit::load("model.pt");  //加载模型
	}
	catch (const c10::Error& e) {
		MessageBox("无法加载model.pt模型");
		return;
	}

	std::vector<int64_t> sizes = { 1, 1, m_img.rows, m_img.cols };  //依次为batchsize、通道数、图像高度、图像宽度
	at::TensorOptions options(at::ScalarType::Byte);
	at::Tensor tensor_image = torch::from_blob(m_img.data, at::IntList(sizes), options);//将opencv的图像数据转为Tensor张量数据
	tensor_image = tensor_image.toType(at::kFloat);//转为浮点型张量数据
	at::Tensor result = module.forward({ tensor_image }).toTensor();//推理

	auto max_result = result.max(1, true);
	auto max_index = std::get<1>(max_result).item<float>();
	CString str;
	str.Format("%.0f", max_index);
	MessageBox(str);
}

最终效果如下:

最后本教程给出了详细代码,MFC部分就不给出了,因为很多人可能不需要这个。如果需要的再联系我吧。

评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

钱彬 (Qian Bin)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值