深度学习图像处理相关代码LibTorch部署详细教程
- 前言
- LibTorch简介
- LibTorch环境安装及问题解决
- LibTorch涉及的Tensor基本操作
- 张量初始化
- 张量变形
- 张量截取
- 张量间操作
- 部署过程
- 测试环境
- 推理过程代码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可以正常调用,这样就可以进行部署代码的编写了。
推理过程代码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<Vec1>(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 <opencv2/opencv.hpp>
#include <torch/torch.h>
#include <torch/script.h>
#include <iostream>
#include "dirent.h"
#include <cstring>
#include <algorithm>
#include <stdlib.h>
#include <string>
#include <windows.h>
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 。有任何问题可以评论区留言讨论 _
参考文献
【1】https://zhuanlan.zhihu.com/p/369930315
【2】https://www.cnblogs.com/allentbky/p/14163898.html