如何用opencv的GPU模块实现算法

一、访存问题


转自: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函数来操作,大部分gpu函数名字与原来CPU名字保持一致,不同的是他们只接收 GpuMat 输入。


另外要记住的是:并非所有的图像类型都可以用于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.gI1 , b.buf 等,除非类型发生变化,GpuMat都将直接使用原来的内容而不重新分配。

1、在GPU中,任何小的数据传输都是一次大的开销,所以应该尽量避免不必要的数据传输。因此应该尽可能的在现有对象上计算(换句话说,不创造新的显存对象,上面已经解释过)。例如,虽然使用算术表达式会让一个公式更浅显易懂但是在GPU代码里,这个步骤是很缓慢的。例如在SSIM例子中需要计算:
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);

2、使用异步调用 (the gpu::Stream)。默认情况下,无论何时你调用一个GPU函数,系统都将等待调用完成并返回后继结果,这就意味着在GPU计算的时候CPU没干活。如果使异步调用,就会意味着它将调用后立即返回,并在后台异步执行,也就是说虽然CPU需要的数据还没从GPU返回,但是并不妨碍CPU进行其它的计算或者调用另一个函数。这是一个小的可优化点。在我们的默认实现中,我们将图像分裂成多个通道,然后为每个通道调用GPU函数。这本来应该是一个平行操作,我们可以建立一个流,通过使用流我们可以异步进行数据分配、传输等各种操作,并且给GPU执行操作方法以便GPU在合适的时候自动调用。例如我们需要传输两幅图像。我们依次调用现成的函数处理它们。该函数还是要等到上传数据完毕才会开始工作,但是在执行第二个函数的时候,原来的输出缓存将直接作为第二个操作的输出参数而不需要传输过程。

定义结构体:

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其实也就那样


关于stream的使用需要进一步的学习。这是官方文档关于stream的说明:

http://docs.opencv.org/modules/gpu/doc/data_structures.html


二、并行化



你可能感兴趣的:(opencv,cuda)