LibTorch部署图像分割算法详细教程(附代码)

深度学习图像处理相关代码LibTorch部署详细教程

  • 前言
  • LibTorch简介
  • LibTorch环境安装及问题解决
  • LibTorch涉及的Tensor基本操作
    • 张量初始化
    • 张量变形
    • 张量截取
    • 张量间操作
  • 部署过程
    • 测试环境
    • PyTorch模型转换
    • 推理过程代码Demo
    • 扩展部分

前言

本文写于调研深度学习部署方法工作中,需要将图像分割模型进行部署。前面博客记录了如何直接打包深度学习模型成exe文件,方便快捷,但是不适合实际工作中作为深度学习模型部署的方法。主要由于打包的方式运行较慢,而且与其余代码的兼容性较差,因此学习了一下LibTorch相关内容,并把使用PyTorch训练的模型成功部署。

LibTorch简介

Libtorch是Pytorch的C++接口,实现了在C++中进行网络训练、网络推理的功能。除此之外,由于Libtorch中的大部份接口都是与Pytorch一致的,所以Libtorch还是一个很强大的张量库,有着类似Pytorch的清晰接口,这在C++中很难得的。如果你用过C++ Tensor库,就会发现写法比较复杂,学习成本高。因为强类型的限制和通用容器类型的缺失,C++相比Python天然更复杂,库设计者因为语言使用习惯,以及为了性能等因素,设计的接口一般都是高效但难用的。而Libtorch采用了与Pytorch类似的函数接口,如果你使用过Pytorch的话,使用Libtorch学习成本很低。

LibTorch环境安装及问题解决

此部分内容本人已在另一博客讲解,欢迎浏览

LibTorch涉及的Tensor基本操作

张量初始化

LibTorch(pytorch c++)的大多数api和PyTorch保持一致,因此,LibTorch中张量的初始化也和PyTorch中的类似。本文介绍四种深度图像编程需要的初始化方法。
第一种,固定尺寸和值的初始化。

//常见固定值的初始化方式
auto b = torch::zeros({3,4});
b = torch::ones({3,4});
b= torch::eye(4);
b = torch::full({3,4},10);
b = torch::tensor({33,22,11});

PyTorch中用[]表示尺寸,而cpp中用{}表示。zeros产生值全为0的张量。ones产生值全为1的张量。eye产生单位矩阵张量。full产生指定值和尺寸的张量。torch::tensor({})也可以产生张量,效果和pytorch的torch.Tensor([])或者torch.tensor([])一样。

第二种,固定尺寸,随机值的初始化方法

//随机初始化
auto r = torch::rand({3,4});
r = torch::randn({3, 4});
r = torch::randint(0, 4,{3,3});

rand产生0-1之间的随机值,randn取正态分布N(0,1)的随机值,randint取[min,max)的随机整型数值。

第三种,从c++的其他数据类型转换而来

int aa[10] = {3,4,6};
std::vector<float> aaaa = {3,4,6};
auto aaaaaaa = torch::tensor(aaaa);
auto aaaaa = torch::from_blob(aa,{3},torch::kFloat);
auto aaa = torch::from_blob(aaaa.data(),{3},torch::kFloat);

PyTorch可以接受从其他数据类型如numpy和list的数据转化成张量。LibTorch同样可以接受其他数据指针,通过from_blob函数即可转换。这个方式在部署中经常用到,如果图像是opencv加载的,那么可以通过from_blob将图像指针转成张量。

第四种,根据已有张量初始化

auto b = torch::zeros({3,4});
auto d = torch::Tensor(b);
d = torch::zeros_like(b);
d = torch::ones_like(b);
d = torch::rand_like(b,torch::kFloat);
d = b.clone();

这里,auto d = torch::Tensor(b)等价于auto d = b,两者初始化的张量d均受原张量b的影响,b中的值发生改变,d也将发生改变,但是b如果只是张量变形,d却不会跟着变形,仍旧保持初始化时的形状,这种表现称为浅拷贝。zeros_like和ones_like顾名思义将产生和原张量b相同形状的0张量和1张量,randlike同理。最后一个clone函数则是完全拷贝成一个新的张量,原张量b的变化不会影响d,这被称作深拷贝。

张量变形

torch改变张量形状,不改变张量存储的data指针指向的内容,只改变张量的取数方式。LibTorch的变形方式和PyTorch一致,有view,transpose,reshape,permute等常用变形。

auto b = torch::full({10},3);
b.view({1, 2,-1});
std::cout<<b;
b = b.view({1, 2,-1});
std::cout<<b;
auto c = b.transpose(0,1);
std::cout<<c;
auto d = b.reshape({1,1,-1});
std::cout<<d;
auto e = b.permute({1,0,2});
std::cout<<e;

.view不是inplace操作,需要加=。变形操作没太多要说的,和PyTorch一样。还有squeeze和unsqueeze操作,也与PyTorch相同。

张量截取

通过索引截取张量,代码如下

auto b = torch::rand({10,3,28,28});
std::cout<<b[0].sizes();//第0张照片
std::cout<<b[0][0].sizes();//第0张照片的第0个通道
std::cout<<b[0][0][0].sizes();//第0张照片的第0个通道的第0行像素 dim为1
std::cout<<b[0][0][0][0].sizes();//第0张照片的第0个通道的第0行的第0个像素 dim为0

除了索引,还有其他操作是常用的,如narrow,select,index,index_select。

std::cout<<b.index_select(0,torch::tensor({0, 3, 3})).sizes();//选择第0维的0,3,3组成新张量[3,3,28,28]
std::cout<<b.index_select(1,torch::tensor({0,2})).sizes(); //选择第1维的第0和第2的组成新张量[10, 2, 28, 28]
std::cout<<b.index_select(2,torch::arange(0,8)).sizes(); //选择十张图片每个通道的前8列的所有像素[10, 3, 8, 28]
std::cout<<b.narrow(1,0,2).sizes();//选择第1维,从0开始,截取长度为2的部分张量[10, 2, 28, 28]
std::cout<<b.select(3,2).sizes();//选择第3维度的第二个张量,即所有图片的第2行组成的张量[10, 3, 28]

index需要单独说明用途。在pytorch中,通过掩码Mask对张量进行筛选是容易的直接Tensor[Mask]即可。但是c++中无法直接这样使用,需要index函数实现,代码如下:

auto c = torch::randn({3,4});
auto mask = torch::zeros({3,4});
mask[0][0] = 1;
std::cout<<c;
std::cout<<c.index({mask.to(torch::kBool)});

有网友提问,这样index出来的张量是深拷贝的结果,也就是得到一个新的张量,那么如何对原始张量的mask指向的值做修改呢。查看torch的api发现还有index_put_函数用于直接放置指定的张量或者常数。组合index_put_和index函数可以实现该需求。

auto c = torch::randn({ 3,4 });
auto mask = torch::zeros({ 3,4 });
mask[0][0] = 1;
mask[0][2] = 1;
std::cout << c;
std::cout << c.index({ mask.to(torch::kBool) });
std::cout << c.index_put_({ mask.to(torch::kBool) }, c.index({ mask.to(torch::kBool) })+1.5);
std::cout << c;

此外python中还有一种常见取数方式tensor[:,0::4]这种在第1维,起始位置为0,间隔4取数的方式,在c++中实现需要借助torch::linspace实现。linspace本身接受三个参数,start,end和step,分别表示起始,终止和间隔。组合前面提到的index_select和linspace即可实现:

auto tensor = torch::randn({ 3,12 });
auto tensor_slice = tensor.index_select(1, torch::linspace(0, tensor.size(1), 4));

张量间操作

拼接和堆叠

auto b = torch::ones({3,4});
auto c = torch::zeros({3,4});
auto cat = torch::cat({b,c},1);//1表示第1维,输出张量[3,8]
auto stack = torch::stack({b,c},1);//1表示第1维,输出[3,2,4]
std::cout<<b<<c<<cat<<stack;

到这读者会发现,从pytorch到libtorch,掌握了[]到{}的变化就简单很多,大部分操作可以直接迁移。

四则运算操作同理,像对应元素乘除直接用*和/即可,也可以用.mul和.div。矩阵乘法用.mm,加入批次就是.bmm。

auto b = torch::rand({3,4});
auto c = torch::rand({3,4});
std::cout<<b<<c<<b*c<<b/c<<b.mm(c.t());

其他一些操作像clamp,min,max这种都和pytorch类似,仿照上述方法可以自行探索。

部署过程

测试环境

当你在电脑上的LIbTorch的环境配置完成,需要用代码测试一下环境是否配置成功,cuda以及cudnn是否可以正常使用。可以复制以下代码添加到cpp文件进行测试。

int main()
{
	//定义使用cuda
	auto device = torch::Device(torch::kCUDA);
	std::cout << "CUDA:" << torch::cuda::is_available();
	std::cout << "CUDNN:  " << torch::cuda::cudnn_is_available() << std::endl;
	std::cout << "GPU(s): " << torch::cuda::device_count() << std::endl;
}

当上述代码前两项返回True,最后一项返回设备GPU个数时,即证明环境已成功配置,cuda,cudnn可以正常调用,这样就可以进行部署代码的编写了。

PyTorch模型转换

PyTorch导出的模型文件是不能直接被LibTorch读取的,因为PyTorch默认导出的后端的序列化是joblib。PyTorch通过JIT搭建了Python和C++的桥梁,我们可以将模型转成TorchScript Module,将Python运行时的部分运行时包裹进去。以下为将PyTorch模型转为LibTorch可以加载的模型代码

import torch

model = Net() #你的模型
model.load_state_dict(torch.load("model/digit.pth", map_location="cpu"))

sample = torch.randn(1, 3, 256, 256) #模型的输入数据大小

trace_model = torch.jit.trace(model, sample)
trace_model.save("model/digit.pt") #将模型结构和参数一起保存

推理过程代码Demo

以下是一个完整的推理过程代码,包括通过OpenCV加载图像,并转为Tensor进行推理操作。

int main()
{
	//定义使用cuda
	auto device = torch::Device(torch::kCUDA);
	//读取图片并展示
	cv::Mat image = cv::imread("E:/深度学习部署相关/TransUNet-main/data/train/images/1.2.826.0.1.3680043.2.461.13267976.60458625.png");
	cv::Size size = image.size();
	std::cout << size;
	//打印三维图像像素值,需要使用以下方式,先定义一个cv::Vec类型Vec1,在通过cv::Mat.at(i, j)[0]访问,具体见下实例
	typedef cv::Vec<uchar, 3> Vecci; //uchar为cv::Mat的数据类型,3为图像通道数。
	for (int i = 52; i < 53; i++)
		{
			for (int j = 371; j < 385; j++)
			{
				cout << "Value0 is:" << image.at<Vecci>(i, j)[0] << endl;
				cout << "Value1 is:" << image.at<Vecci>(i, j)[1] << endl;
				cout << "Value2 is:" << image.at<Vecci>(i, j)[2] << endl;
			}
		}
	cv::imshow("img", image);
	cv::waitKey(0);

	//读取标贴并展示
	cv::Mat lable = cv::imread("E:/深度学习部署相关/TransUNet-main/data/train/labels/1.2.826.0.1.3680043.2.461.13267976.60458625.png");
	cv::Mat gray;
	cv::cvtColor(lable, gray, cv::COLOR_BGR2GRAY);
	cv::normalize(gray, gray, 0, 255, cv::NORM_MINMAX);
	cv::imshow("label", gray);
	cv::waitKey(0);

	//缩放至指定大小
	cv::resize(image, image, cv::Size(256, 256));
	//转成张量
	auto input_tensor = torch::from_blob(image.data, { image.rows, image.cols, 3 }, torch::kByte).permute({ 2, 0, 1 }).unsqueeze(0).to(torch::kFloat32);
	//加载模型
	auto model = torch::jit::load("E:/深度学习部署相关/LibTorch/Project1/TransUNet.pt");
	model.to(device);
	model.eval();
	//前向传播
	auto output = model.forward({ input_tensor.to(device) }).toTensor();
	output = torch::squeeze(torch::argmax(torch::softmax(output, 1), 1), 0);
	std::cout << output.sizes() << std::endl;
	output = output.to(torch::kU8).to(torch::kCPU);
	//将tensor转为cv::Mat格式,进行展示
	cv::Mat Img(output.sizes()[0], output.sizes()[1], CV_8U, output.data_ptr());
	cv::resize(Img, Img, size);
	cv::normalize(Img, Img, 0, 255, cv::NORM_MINMAX);
	cv::imshow("result", Img);
	cv::waitKey(0);
	return 0;
	}

扩展部分

上述部分涉及了自然图像进行深度学习处理的全过程,但是不是所有的图像数据都是自然图像,OpenCV并不适合加载所有的像素数据,作者就是需要在工作中加载二进制存储的非标准图像数据,这时需要如何加载数据并转换成Tensor进行模型推理呢,过程还是较为复杂,笔者在此由于数据类型转换卡了很久,最后终于成功运行。以下是相关部分代码,需要的读者可以参考。

#include 
#include 
#include  
#include 
#include "dirent.h"
#include 
#include 
#include 
#include 
#include 
using namespace std;

//读取二进制图像文件,并将其值归一化到0~255
int* ReadSlice(std::string file, const size_t size)
{
	std::ifstream ifs(file, std::ios::binary);
	signed short* img = new signed short[size];
	if (ifs.is_open())
	{
		ifs.read((char*)img, sizeof(int16_t) * size);
		ifs.close();
	}
	else
	{
		std::cout << "Unable to open file" << std::endl;
	}
	signed short maxValue = *max_element(img, img + size);
	signed short minValue = *min_element(img, img + size);

	int* newImg = new int[size];
	for (int i = 0; i < size; i++)
	{
		newImg[i] = int((float(img[i] - minValue) / float(maxValue - minValue)) * 255);
	}

	cout << "success loaded img" << endl;
	return newImg;
}


int main(int argc, char* argv[])
{
	// 检查参数个数
	if (argc != 3)
	{
		cout << "Usage: " << argv[0] << " folder_path" << endl;
		return 1;
	}

	// 获取文件夹路径
	string path = argv[1];
	int size = atoi(argv[2]);
	cout << path << endl;
	cout << size << endl;
	//int size = 562500;

	// 打开文件夹
	DIR* dir = opendir(path.c_str());

	if (dir == NULL)
	{
		cout << "Failed to open directory!" << endl;
		return 1;
	}

	auto device = torch::Device(torch::kCUDA);
	// 遍历文件夹
	struct dirent* entry;
	while ((entry = readdir(dir)) != NULL)
	{
		// 排除 . 和 .. 目录
		if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0 || string(entry->d_name) == "ImageParam.ini")
		{
			continue;
		}

		// 输出文件名
		string filePath = path + "\\" + entry->d_name;
		cout << filePath << endl;
		cout << entry->d_name << endl;
		// 读取图像数据,并将其存储在数组结构中
		int* image = ReadSlice(filePath, size);

		const int length = sqrt(size);
		cv::Size imageSize (int(sqrt(size)), int(sqrt(size)));
		// 新建cv::Mat数据结构,并用读取的数组值进行赋值,注意cv::Mat的数据类型要前后保持一致
		cv::Mat Img(int(sqrt(size)), int(sqrt(size)), CV_8UC1);
		
		typedef cv::Vec<uchar, 3> Vec3c;
		for (int i = 0; i < Img.rows; i++)
		{
			for (int j = 0; j < Img.cols; j++)
			{
				//cout << int(image[i * length + j]) << endl;
				Img.at<uchar>(i, j) = int(image[i * length + j]);
			}
		}
		//将一维cv::Mat进行拼接,生成三维cv::Mat数据
		vector<cv::Mat> ImgMerge = { Img, Img, Img };
		cv::Mat ImgCopy = cv::Mat::zeros(int(sqrt(size)), int(sqrt(size)), CV_8UC3);
		cv::merge(ImgMerge, ImgCopy);

		cv::imwrite("E:\\demo.png", Img);
		cout << ImgCopy.size() << endl;
		cv::imshow("label", Img);
		cv::waitKey(0);
		cv::resize(ImgCopy, ImgCopy, cv::Size(256, 256));
		return 0}

总结: 至此LibTorch整体流程已经跑通,希望大家写代码可以顺顺利利,少出bug 。有任何问题可以评论区留言讨论 _LibTorch部署图像分割算法详细教程(附代码)_第1张图片
参考文献
【1】https://zhuanlan.zhihu.com/p/369930315
【2】https://www.cnblogs.com/allentbky/p/14163898.html

你可能感兴趣的:(深度学习,安装环境,图像处理,深度学习,LibTorch)