转自:http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/gpu/gpu-basics-similarity/gpu-basics-similarity.html
开发的GPU模块尽可能多的与CPU对应,这样才能方便移植。
GPU代表图形处理单元。最开始是为渲染各种图形场景而建立,这些场景是基于大量的矢量数据建立的。由于矢量图形的特殊性,数据不需要以串行的方式一步一步执行的,而是并行的方式一次性渲染大量的数据。从GPU的结构上来说,不像CPU基于数个寄存器和高速指令集,GPU一般有数百个较小的处理单元。这些处理单元每一个都比CPU的核心慢很多很多。然而,它的力量在于它的数目众多,能够同时进行大量运算。在过去的几年中已经有越来越多的人尝试用GPU来进行图形渲染以外的工作。这就产生了GPGPU(general-purpose computation on graphics processing units)的概念。
图像处理器有它自己的内存,一般称呼为显存。当你从硬盘驱动器读数据并产生一个 Mat 对象的时候,数据是放在普通内存当中的(由CPU掌管)CPU可以直接操作内存, 然而GPU不能这样,对于电脑来说GPU只是个外设,它只能操作它自己的显存,当计算时,需要先让CPU将用于计算的信息转移到GPU掌管的显存上。 这是通过一个上传过程完成,需要比访问内存多很多的时间。而且最终的计算结果将要回传到你的系统内存处理器才能和其他代码交互,由于传输的代价高昂,所以注定移植太小的函数到GPU上并不会提高效率。
Mat对象仅仅存储在内存或者CPU缓存中。为了得到一个GPU能直接访问的opencv 矩阵你必须使用GPU对象 GpuMat 。它的工作方式类似于2维 Mat,唯一的限制是你不能直接引用GPU函数(因为它们本质上是完全不同的代码,不能混合引用)。要传输*Mat*对象到*GPU*上并创建GpuMat时需要调用上传函数,在回传时,可以使用简单的重载赋值操作符或者调用下载函数。
Mat I1; // 内存对象,可以用imread来创建
gpu::GpuMat gI; // GPU 矩阵 - 现在为空
gI1.upload(I1); //将内存数据上传到显存中
I1 = gI1; //回传, gI1.download(I1) 也可以
另外要记住的是:并非所有的图像类型都可以用于GPU算法中。很多时候,GPU函数只能接收单通道或者4通道的uchar或者float类型(CV_8UC1,CV_8UC4,CV_32FC1,CV_32FC4),GPU函数不支持双精度。如果你试图向这些函数传入非指定类型的数据时,这些函数会抛出异常或者输出错误信息。这些函数的文档里大都说明了接收的数据类型。如果函数只接收4通道或者单通道的图像而你的数据类型刚好是3通道的话,你能做的就是两件事:1.增加一个新的通道(使用char类型)或 2.将图像的三个通道切分为三个独立的图像,为每个图像调用该函数。不推荐使用第一个方法,因为有些浪费内存。
对于不需要在意元素的坐标位置的某些函数,快速解决办法就是将它直接当作一个单通道图像处理。然而,对于GaussianBlur就不能这么用,需要使用分离通道的方法。
知道这些知识就已经可以让你的GPU开始运行代码。但是你也可能发现GPU“加速”后的代码仍然可能会低于你的CPU执行速度。
优化
因为大量的时间都耗费在传输数据到显存这样的步骤了,大量的计算时间被消耗在内存分配和传输上,GPU的高计算能力根本发挥不出来,我们可以利用 gpu::Stream 来进行异步传输,从而节约一些时间。
GPU上的显存分配代价是相当大的。因此如果可能的话,应该尽可能少的分配新的内存。如果你需要创建一个调用多次的函数,就应该在一开始分配全部的内存,而且仅仅分配一次。在第一次调用的时候可以创建一个结构体包含所有将使用局部变量。 对于PSNR例子:
struct BufferPSNR // 优化版本
{ // 基于GPU的数据分配代价是高昂的。所以使用一个缓冲区来改善这点:分配一次,以后重用。
gpu::GpuMat gI1, gI2, gs, t1,t2;
gpu::GpuMat buf;
};
在主程序中创建一个实例:
BufferPSNR bufferPSNR;
最后每次调用函数时都使用这个结构:
double getPSNR_GPU_optimized(const Mat& I1, const Mat& I2, BufferPSNR& b)
b.t1 = 2 * b.mu1_mu2 + C1;
在这个表达式的调用过程中必然会有一个隐性内存分配过程,调用乘法的结果必然要用一个临时对象储存,然后才能和*C1*相加。如果直接用表达式写GPU代码,就必然会创建一个临时变量来储存乘积的矩阵,然后加上 C1 值并储存在 t1 。为了避免这种额外的开销,我们应该使用GPU处理函数代替算术表达式,从而避免额外创建不必要的临时对象:
gpu::multiply(b.mu1_mu2, 2, b.t1); //b.t1 = 2 * b.mu1_mu2 + C1;
gpu::add(b.t1, C1, b.t1);
定义结构体:
struct BufferMSSIM // Optimized GPU versions
{ // Data allocations are very expensive on GPU. Use a buffer to solve: allocate once reuse later.
gpu::GpuMat gI1, gI2, gs, t1,t2;
gpu::GpuMat I1_2, I2_2, I1_I2;
vector vI1, vI2;
gpu::GpuMat mu1, mu2;
gpu::GpuMat mu1_2, mu2_2, mu1_mu2;
gpu::GpuMat sigma1_2, sigma2_2, sigma12;
gpu::GpuMat t3;
gpu::GpuMat ssim_map;
gpu::GpuMat buf;
};
函数
Scalar getMSSIM_GPU_optimized( const Mat& i1, const Mat& i2, BufferMSSIM& b)
{
int cn = i1.channels();
const float C1 = 6.5025f, C2 = 58.5225f;
/***************************** INITS **********************************/
b.gI1.upload(i1);
b.gI2.upload(i2);
gpu::Stream stream;
stream.enqueueConvert(b.gI1, b.t1, CV_32F);
stream.enqueueConvert(b.gI2, b.t2, CV_32F);
gpu::split(b.t1, b.vI1, stream);
gpu::split(b.t2, b.vI2, stream);
Scalar mssim;
for( int i = 0; i < b.gI1.channels(); ++i )
{
gpu::multiply(b.vI2[i], b.vI2[i], b.I2_2, stream); // I2^2
gpu::multiply(b.vI1[i], b.vI1[i], b.I1_2, stream); // I1^2
gpu::multiply(b.vI1[i], b.vI2[i], b.I1_I2, stream); // I1 * I2
gpu::GaussianBlur(b.vI1[i], b.mu1, Size(11, 11), 1.5, 0, BORDER_DEFAULT, -1, stream);
gpu::GaussianBlur(b.vI2[i], b.mu2, Size(11, 11), 1.5, 0, BORDER_DEFAULT, -1, stream);
gpu::multiply(b.mu1, b.mu1, b.mu1_2, stream);
gpu::multiply(b.mu2, b.mu2, b.mu2_2, stream);
gpu::multiply(b.mu1, b.mu2, b.mu1_mu2, stream);
gpu::GaussianBlur(b.I1_2, b.sigma1_2, Size(11, 11), 1.5, 0, BORDER_DEFAULT, -1, stream);
gpu::subtract(b.sigma1_2, b.mu1_2, b.sigma1_2, stream);
//b.sigma1_2 -= b.mu1_2; - This would result in an extra data transfer operation
gpu::GaussianBlur(b.I2_2, b.sigma2_2, Size(11, 11), 1.5, 0, BORDER_DEFAULT, -1, stream);
gpu::subtract(b.sigma2_2, b.mu2_2, b.sigma2_2, stream);
//b.sigma2_2 -= b.mu2_2;
gpu::GaussianBlur(b.I1_I2, b.sigma12, Size(11, 11), 1.5, 0, BORDER_DEFAULT, -1, stream);
gpu::subtract(b.sigma12, b.mu1_mu2, b.sigma12, stream);
//b.sigma12 -= b.mu1_mu2;
//here too it would be an extra data transfer due to call of operator*(Scalar, Mat)
gpu::multiply(b.mu1_mu2, 2, b.t1, stream); //b.t1 = 2 * b.mu1_mu2 + C1;
gpu::add(b.t1, C1, b.t1, stream);
gpu::multiply(b.sigma12, 2, b.t2, stream); //b.t2 = 2 * b.sigma12 + C2;
gpu::add(b.t2, C2, b.t2, stream);
gpu::multiply(b.t1, b.t2, b.t3, stream); // t3 = ((2*mu1_mu2 + C1).*(2*sigma12 + C2))
gpu::add(b.mu1_2, b.mu2_2, b.t1, stream);
gpu::add(b.t1, C1, b.t1, stream);
gpu::add(b.sigma1_2, b.sigma2_2, b.t2, stream);
gpu::add(b.t2, C2, b.t2, stream);
gpu::multiply(b.t1, b.t2, b.t1, stream); // t1 =((mu1_2 + mu2_2 + C1).*(sigma1_2 + sigma2_2 + C2))
gpu::divide(b.t3, b.t1, b.ssim_map, stream); // ssim_map = t3./t1;
stream.waitForCompletion();
Scalar s = gpu::sum(b.ssim_map, b.buf);
mssim.val[i] = s.val[0] / (b.ssim_map.rows * b.ssim_map.cols);
}
return mssim;
}
经实际测试,使用异步stream带来的提高不足15%,这里的异步不是真正意义上的并行,如果需要发挥GPU的全部性能,只能自己编写内核函数,即cuda编程,这样才能够实实际际利用gpu的并行特效。
这是测试代码,基于opencv3.0,gpu模块有变动,包括命名空间、个别函数的用法均发生改变
先声明命名空间:
using cv::Mat;
using cv::Scalar;
using cv::Size;
using cv::cuda::GpuMat;
namespace gpu = cv::cuda;
无stream的MSSIM函数:
Scalar getMSSIM_GPU_optimized_noStream( const Mat& i1, const Mat& i2, BufferMSSIM& b)
{
int cn = i1.channels();
const float C1 = 6.5025f, C2 = 58.5225f;
/***************************** INITS **********************************/
b.gI1.upload(i1);
b.gI2.upload(i2);
b.gI1.convertTo(b.t1, CV_32F);
b.gI2.convertTo(b.t2, CV_32F);
gpu::split(b.t1, b.vI1);
gpu::split(b.t2, b.vI2);
Scalar mssim;
//cuda模块调用滤波函数的步骤:先创建一个滤波器,然后再调用该滤波器
cv::Ptr gauss = gpu::createGaussianFilter(CV_32F, CV_32F, Size(11, 11), 1.5, 0, cv::BORDER_DEFAULT,-1); //创建高斯滤波器
for( int i = 0; i < b.gI1.channels(); ++i )
{
gpu::multiply(b.vI2[i], b.vI2[i], b.I2_2, 1,-1); // I2^2
gpu::multiply(b.vI1[i], b.vI1[i], b.I1_2, 1,-1); // I1^2
gpu::multiply(b.vI1[i], b.vI2[i], b.I1_I2, 1,-1); // I1 * I2
gauss->apply(b.vI1[i], b.mu1); //高斯滤波
gauss->apply(b.vI2[i], b.mu2); //高斯滤波
gpu::multiply(b.mu1, b.mu1, b.mu1_2, 1.0, -1);
gpu::multiply(b.mu2, b.mu2, b.mu2_2, 1.0, -1);
gpu::multiply(b.mu1, b.mu2, b.mu1_mu2, 1.0, -1);
gauss->apply(b.I1_2, b.sigma1_2); //高斯滤波
gpu::subtract(b.sigma1_2, b.mu1_2, b.sigma1_2, cv::noArray(), -1); //subtract采取in-place操作,相减结果直接存放在b.sigma1_2
//注意 : b.sigma1_2 -= b.mu1_2; 该语句会造成额外的数据传输,相减结果先缓存在两者以外的位置,然后再取回b.sigma1_2
gauss->apply(b.I2_2, b.sigma2_2); //高斯滤波
gpu::subtract(b.sigma2_2, b.mu2_2, b.sigma2_2, cv::noArray(), -1);
gauss->apply(b.I1_I2, b.sigma12); //高斯滤波
gpu::subtract(b.sigma12, b.mu1_mu2, b.sigma12, cv::noArray(), -1);
gpu::multiply(b.mu1_mu2, 2, b.t1, 1.0, -1);
//同样道理,这样做 b.t1 = 2 * b.mu1_mu2 + C1; 是耗费时间的
gpu::add(b.t1, C1, b.t1, cv::noArray(), -1);
gpu::multiply(b.sigma12, 2, b.t2, 1.0, -1);
//耗费时间的做法 : b.t2 = 2 * b.sigma12 + C2;
gpu::add(b.t2, C2, b.t2, cv::noArray(), -1);
gpu::multiply(b.t1, b.t2, b.t3, 1.0, -1); // t3 = ((2*mu1_mu2 + C1).*(2*sigma12 + C2))
gpu::add(b.mu1_2, b.mu2_2, b.t1, cv::noArray(), -1);
gpu::add(b.t1, C1, b.t1, cv::noArray(), -1);
gpu::add(b.sigma1_2, b.sigma2_2, b.t2, cv::noArray(), -1);
gpu::add(b.t2, C2, b.t2, cv::noArray(), -1);
gpu::multiply(b.t1, b.t2, b.t1, 1.0, -1); // t1 =((mu1_2 + mu2_2 + C1).*(sigma1_2 + sigma2_2 + C2))
gpu::divide(b.t3, b.t1, b.ssim_map, 1.0, -1); // ssim_map = t3./t1;
Scalar s = gpu::sum(b.ssim_map, b.buf);
mssim.val[i] = s.val[0] / (b.ssim_map.rows * b.ssim_map.cols);
}
return mssim;
}
有stream的MSSIM函数:
Scalar getMSSIM_GPU_optimized( const Mat& i1, const Mat& i2, BufferMSSIM& b)
{
int cn = i1.channels();
const float C1 = 6.5025f, C2 = 58.5225f;
/***************************** INITS **********************************/
b.gI1.upload(i1);
b.gI2.upload(i2);
gpu::Stream stream;
b.gI1.convertTo(b.t1, CV_32F,stream);
b.gI2.convertTo(b.t2, CV_32F,stream);
gpu::split(b.t1, b.vI1, stream);
gpu::split(b.t2, b.vI2, stream);
Scalar mssim;
//cuda模块调用滤波函数的步骤:先创建一个滤波器,然后再调用该滤波器
cv::Ptr gauss = gpu::createGaussianFilter(CV_32F, CV_32F, Size(11, 11), 1.5, 0, cv::BORDER_DEFAULT,-1); //创建高斯滤波器
for( int i = 0; i < b.gI1.channels(); ++i )
{
gpu::multiply(b.vI2[i], b.vI2[i], b.I2_2, 1,-1,stream); // I2^2
gpu::multiply(b.vI1[i], b.vI1[i], b.I1_2, 1,-1,stream); // I1^2
gpu::multiply(b.vI1[i], b.vI2[i], b.I1_I2, 1,-1,stream); // I1 * I2
gauss->apply(b.vI1[i], b.mu1, stream); //高斯滤波
gauss->apply(b.vI2[i], b.mu2, stream); //高斯滤波
gpu::multiply(b.mu1, b.mu1, b.mu1_2, 1.0, -1, stream);
gpu::multiply(b.mu2, b.mu2, b.mu2_2, 1.0, -1, stream);
gpu::multiply(b.mu1, b.mu2, b.mu1_mu2, 1.0, -1, stream);
gauss->apply(b.I1_2, b.sigma1_2, stream); //高斯滤波
gpu::subtract(b.sigma1_2, b.mu1_2, b.sigma1_2, cv::noArray(), -1, stream); //subtract采取in-place操作,相减结果直接存放在b.sigma1_2
//注意 : b.sigma1_2 -= b.mu1_2; 该语句会造成额外的数据传输,相减结果先缓存在两者以外的位置,然后再取回b.sigma1_2
gauss->apply(b.I2_2, b.sigma2_2, stream); //高斯滤波
gpu::subtract(b.sigma2_2, b.mu2_2, b.sigma2_2, cv::noArray(), -1, stream);
gauss->apply(b.I1_I2, b.sigma12, stream); //高斯滤波
gpu::subtract(b.sigma12, b.mu1_mu2, b.sigma12, cv::noArray(), -1, stream);
gpu::multiply(b.mu1_mu2, 2, b.t1, 1.0, -1, stream);
//同样道理,这样做 b.t1 = 2 * b.mu1_mu2 + C1; 是耗费时间的
gpu::add(b.t1, C1, b.t1, cv::noArray(), -1, stream);
gpu::multiply(b.sigma12, 2, b.t2, 1.0, -1, stream);
//耗费时间的做法 : b.t2 = 2 * b.sigma12 + C2;
gpu::add(b.t2, C2, b.t2, cv::noArray(), -1, stream);
gpu::multiply(b.t1, b.t2, b.t3, 1.0, -1, stream); // t3 = ((2*mu1_mu2 + C1).*(2*sigma12 + C2))
gpu::add(b.mu1_2, b.mu2_2, b.t1, cv::noArray(), -1, stream);
gpu::add(b.t1, C1, b.t1, cv::noArray(), -1, stream);
gpu::add(b.sigma1_2, b.sigma2_2, b.t2, cv::noArray(), -1, stream);
gpu::add(b.t2, C2, b.t2, cv::noArray(), -1, stream);
gpu::multiply(b.t1, b.t2, b.t1, 1.0, -1, stream); // t1 =((mu1_2 + mu2_2 + C1).*(sigma1_2 + sigma2_2 + C2))
gpu::divide(b.t3, b.t1, b.ssim_map, 1.0, -1, stream); // ssim_map = t3./t1;
stream.waitForCompletion(); //等待所有stream完成
Scalar s = gpu::sum(b.ssim_map, b.buf);
mssim.val[i] = s.val[0] / (b.ssim_map.rows * b.ssim_map.cols);
}
return mssim;
}
对一个2000*2000的Mat,有stream:25ms,无stream:29ms,快了13%
对一个3000*3000的Mat,有stream:51ms,无stream:49ms,反而慢了!
证明steam其实也就那样
http://docs.opencv.org/modules/gpu/doc/data_structures.html