目录
[4.1 图像预处理讲解](#4.1 图像预处理讲解)
一、前言:
之前实现了基于win环境中的python的加速部署过程,现在将c++实现过程进行记录,这次记录会非常详细。总的来说只要整个过程的思路捋清楚,保证思路是正确的,做起工作来就比较有方向性,开始记录。
二、关于环境安装:
环境:wsl2+Ubuntu2004+cuda113+cudnn8.9.5.30+tensrort8.6.1.6+opencv4.2.0
主要要安装的是cuda和cudnn和tensorrt。确定好版本,我本地cuda是11.7,cuda可以向下兼容,所以我wsl中使用11.3也是支持的。参考博客:【教程】Install NVIDIA TensorRT on WSL 在WSL上安装英伟达TensorRT_wsl tensorrt-CSDN博客,win11 WSL ubuntu安装CUDA、CUDNN、TensorRT最有效的方式-CSDN博客,这两个博客互补一下。
tips:1、安装位置最好新建一个文件夹,放在一起,在设定环境变量的时候会方便些。
2、opencv4版本会和libtorch1.10冲突,如果没有装libtorch之前opencv是好的,装了libtorch发现这两个库都用不了,大概率是版本冲突了(一踩坑,心态崩了一天时间,sad)但是不用libtorch也完全不影响推理。
三、首先记录下自己的几个问题
问题:c++部署和python部署的区别?
我最开始个人理解可能c++会更多设计自己手动开辟回收内存,在我实现c++部署之后,会过头看python实现过程,发现python也是手动开辟回收内存的,所以他们最重要的区别到底在哪里呢?
回答:最重要的东西确实还是内存管理上,然后自然而然的会影响性能,还有系统集成中。 在内存管理方面:
1、C++版本需要显式地管理所有资源的生命周期,包括内存分配和释放。
2、Python版本虽然也进行了一些手动内存管理,但仍然依赖于Python的垃圾回收机制来处理大部分对象的清理。
3、C++版本提供了更细粒度的控制,可以精确地控制何时分配和释放资源。
4、Python版本在语法上更简洁,并且某些资源管理被抽象化,使得代码更容易编写和维护。
性能方面:我理解在一些计算开销没有非常限制的情况下,或者执行次数不多的情况下两者区别不会拉太卡,但是涉及到性能的极致使用的情况下,肯定是c++性能要高出python。
1、C++:通常能提供更高的性能,尤其是在低延迟场景中。直接编译为机器码,没有解释器开销。
2、Python:有解释器开销,但在许多情况下,性能差异可能不明显,特别是当大部分计算都在GPU上进行时。
生态集成方面:
python更加依赖于python开发环境,而c++可以编译为动态库,限制更少一些。总的来说在工业应用中肯定吃c++更加方便些。
四、正文开始
前处理主要分为两部分,第一部分是对image的格式的转换,即cv::imread读取上来的单张图片要转为符合深度学习torch中dataload中读取上来的nchw格式的数据。第二部分就是将这样格式的数据从cpu传输到GPU中。第二部分相对操作固定一些。
Opencv读取图片格式转换为深度学习图片输入格式
一般有两方面需要转换,第一方面是格式,第二方面是尺寸。一般来说应该是先进行格式转换,然后在进行尺寸变化,因为尺寸变化要是涉及到放缩的话,uint8的数据空间可能会一点,可能会有一些精度的损失。
使用opencv中cv::imread读取上来的image一般保存在cv::Mat中,OpenCV 中的 cv::Mat 格式如下:
-
数据类型 :
cv::Mat
默认使用 8 位无符号整数(CV_8U
)存储图像数据。 -
通道顺序 :
cv::Mat
默认使用 BGR 顺序存储图像通道。 -
形状 :
cv::Mat
通常具有二维或三维矩阵结构,对应于(height, width, channels)
。
而深度学习一般需要的输入数据格式如下:
-
数据类型 :深度学习模型通常使用 32 位浮点数(
float32
)存储图像数据。 -
通道顺序:深度学习模型通常使用 RGB 顺序存储图像通道。
-
形状 :深度学习模型的输入形状通常为
(batch_size, channels, height, width)
,即 NCHW 格式。
以上是格式方面的区别,往往还有尺寸方面的区别。yolov5一般要求输入尺寸大小为640*640大小。我们还需要将图片进行缩放。注意:是等比例缩放。
前处理流程和代码:
4.1 图像预处理讲解
首先创建处理图像的函数:
cv::Mat preImage(cv::Mat& rawBGRImage,int inputH, int inputW)
我们创建一个preImage的函数,返回类型是cv::Mat类型,参数输入是一个cv::Mat类型的rawBGRImage的引用,同时输入两个整数,代表深度学习输入的大小,这里为640*640。
第一步:
1、BGR---->RBG
// BGR --> RGB
cv::cvtColor(oriImage,image,cv::COLOR_BGR2RGB);
2、等比例放缩图片(涉及到短边的填充)
首先获得原始图像的h和w;假设1280*1920
int ori_h = oriImage.rows;
int ori_w = oriImage.cols;
std::cout<<"原始图片的size是"<<oriImage.size<<std::endl;
两边不一样长的图片要保证长边缩小到640,短边进行填充操作。
// 取最小的因子,以保证长的一边被正确缩放,短边过度缩放可在后面使用padding填充
// 这两个结果肯定小于1,使用整除会将结果截断为0
double r = std::min(static_cast<double>(inputH) / ori_h, static_cast<double>(inputW) / ori_w);
std::cout<<"缩放因子 r 是"<< r <<std::endl;
int new_h = std::round(ori_h * r);//四舍五入
int new_w = std::round(ori_w * r);
std::cout<<"新图片的size是"<<new_h <<","<<new_w<<std::endl;
计算填充大小,640减去新图像的边长
//计算padding的大小,
int dh = inputH - new_h;
int dw = inputW - new_w;
std::cout<<"需要填充的size是"<<dh <<","<<dw<<std::endl;
// 使用双线性插值将图像进行等比例缩放
cv::resize(image,image,cv::Size(new_h,new_w),0, 0, cv::INTER_LINEAR);
此时得到了一幅(427,640)的图像,需要对h边进行填充
//判断是否需要填充
if(dh > 0 || dw > 0){
cv::resize(image,image,cv::Size(new_h,new_w),0, 0, cv::INTER_LINEAR);
计算填充的像素
// Pad the short side with (128,128,128)
int top = dh / 2;
int bottom = dh - top;
int left = dw / 2;
int right = dw - left;
std::cout<<"方框是"<<top <<","<<bottom<<","<<left<<","<<right<<std::endl;
进行填充,其中top,bottom,left,right等代表填充到图像上下左右的像素行数
//扩充边界
cv::copyMakeBorder(image, image, left, right, top, bottom, cv::BORDER_CONSTANT, cv::Scalar(128, 128, 128));
std::cout<<"扩充边界之后的图片的size是"<<image.size<<std::endl;
}
得到一幅填充到640*640大小的图像
3、归一化
image.convertTo(image, CV_32F); //自动转,如果原图是32f,只拷贝,不做转换。
image /= 255.0; // 8u转32f之后,原始像素值还是不变的,只是可表示范围变大了。
4、HWC---->NCHW
// HWC to CHW format:并处理成cv中的dnn多维格式数据,类似于tensor?
cv::Mat imageCHW;
cv::dnn::blobFromImage(image, imageCHW, 1.0, cv::Size(), cv::Scalar(), false, false);//这种缩放会影响图像精度
//该函数默认bs维度=1。cv::dnn::blobFromImages函数可以自由设定bs维度的值
std::cout << "NCHW shape: " << imageCHW.size << std::endl;
函数介绍:
cv::dnn::blobFromImage
函数是 OpenCV 中的一个实用工具,用于将图像转换为深度学习模型所需的多维格式(通常是 NCHW,即 batch size、channels、height、width)。这是深度学习中常用的数据格式。
cv::Mat blobFromImage(
const cv::Mat& image, //输入图像
double scalefactor = 1.0, //缩放因子,用于缩放图像的每个像素值。默认值是 1.0
const cv::Size& size = cv::Size(),//目标图像的大小。cv::Size(width, height) 格式。如果为 Size(),则保持原始图像大小。
const cv::Scalar& mean = cv::Scalar(),//用于减去的均值向量,cv::Scalar(mean_r, mean_g, mean_b) 格式。用于均值归一化。
bool swapRB = false,//是否交换R和B通道
bool crop = false,//是否在缩放后裁剪图像。如果设置为 true,则图像会被裁剪以适应目标大小
int ddepth = CV_32F//默认32位
);
5、将图像转为一维向量格式并将整理为连续内存
//此处还待确认,是否一定需要转为一维向量。实际测试下来发现转或不转都可以识别。
cv::Mat imageNCHW = imageCHW.reshape(1, 1);
std::cout << "zuihou d NCHW shape: " << imageNCHW.size << std::endl;
// Convert the image to row-major order, also known as "C order":
// 复制矩阵 转换位行主序(C order)存储。意味着数据在内存中是按行连续存储的
// 某些深度学习框架或推理引擎可能要求输入数据是连续存储的
cv::Mat imageRowMajor;
imageNCHW.copyTo(imageRowMajor);
4.2图像内存开辟与传递
首先设定图像大小并计算数据缓存区,然后将数据从cpu复制到gpu。
// 图像前处理函数实现
void YoLov5TRT::preprocessImage(const cv::Mat& image, float* gpu_input) {
cv::Mat Image = image;
// preImage函数中对图片进行详细前处理
cv::Mat risezed_image = preImage(Image, H, W);
size_t image_size = risezed_image.total() * risezed_image.channels(); // 计算输入大小,用来分配缓存区
std::vector<float> input_data(image_size); // 分配输入数据缓冲区
// 5. 使用 memcpy 复制数据
std::memcpy(input_data.data(), risezed_image.data, image_size * sizeof(float));
// 6. 异步复制数据到 GPU
cudaMemcpyAsync(gpu_input, input_data.data(), image_size * sizeof(float), cudaMemcpyHostToDevice, stream_);
}