LibTorch部署图像处理相关算法详细教程(附代码)

news/2024/10/17 14:26:27/

深度学习图像处理相关代码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()
{//定义使用cudaauto 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()
{//定义使用cudaauto 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


http://www.ppmy.cn/news/239648.html

相关文章

实习笔试准备(2)

1 第二题 1.1 题目描述 旅行者穿越沙漠的过程中需要不断地消耗携带的饮用水&#xff0c;到达终点前会经过几个绿洲&#xff0c;每个绿洲均设有水分补给站可以为旅行者提供水分补给并收取一定的费用。 沿途共有n个补给站&#xff0c;每个补给站收取的费用都一样&#xff0c;但…

vue中vuex状态管理

一、安装 cnpm i vuex --save-dev二、引入&#xff08;main.js&#xff09; import store from ./storenew Vue({el: #app,store,//注册components: { App },template: <App/> })三、vuex配置 src > store > index.js&#xff08;src下创建store目录&#xff0c;…

私有化部署低代码开发工具:jvs-rules 规则引擎决策流参数说明

JVS规则引擎决策调用 通过决策流水号查询入参变量 [请求参数]决策流 ​ GET/mgr/risk//test/parameter/flow/{no} 请求数据类型 application/x-www-form-urlencoded 响应数据类型 [ "*/*" ] 请求参数 参数名称 参数说明 请求类型 是否必须 数据类型 sch…

DIY电脑配置选择的常见误区有哪些?

1.cpu能尽量买新不买旧、能买带k的可超频的就不要选便宜一两百的带F的或者低主频的&#xff1b;理由如下&#xff1a;cpu制作其实是按照最高级诸如i9去制作的&#xff0c;成品出来之后&#xff0c;经过检测完整的体质好的i9 k系列。核心有瑕疵的屏蔽掉改成i7系列、只能用六核的…

【装机知识】机箱知识整理

虽然机箱&#xff08;鞋盒&#xff09;不会影响电脑的性能&#xff0c;但还是有一些门道的。 主要参数 尺寸 选机箱的第一要素就是尺寸了&#xff0c;要保证机箱能装下所有需要的硬件。 按照大小分&#xff0c;机箱有大机箱&#xff08;全塔&#xff09;、中机箱&#xff08;中…

千云物流 -redis集群安装使用

redis使用的场景 需要满足120万司机用户的缓存, 满足车辆当前位置和车辆服务key存储司机手机号码对应的存储以及存储司机key的信息保证redis能更好的提供服务,避免单点故障对应会话消息的缓存保存。以及临时会话缓存的key。基于im的构建。综合所得:需要的key会上亿。需要稳…

“AI Earth”人工智能创新挑战赛:助力精准气象和海洋预测Baseline[2]:数据探索性分析(温度风场可视化)、CNN+LSTM模型建模

【机器学习入门与实践】入门必看系列,含数据挖掘项目实战:模型融合、特征优化、特征降维、探索性分析等,实战带你掌握机器学习数据挖掘 专栏详细介绍:【机器学习入门与实践】合集入门必看系列,含数据挖掘项目实战:数据融合、特征优化、特征降维、探索性分析等,实战带你掌…

大疆 三维建模 正射影像 价格

2.65万的精灵 4 RTK 0.7万块的笔记本(16G内存及计算能力在3.0及以上的 NVidia显卡>4G显存) 2.8万的大疆智图软件(有效期为永久) 总共6.14万