文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

Goal

在“使用openCV进行视频输入和相似性测量”教程中我已经展示了使用PSNR和SSIM方法检测两张图片间的相似性. 并且你可以看到进行这些过程相当耗时,尤其是SSIM方法. 然而, 如果对于使用CPU的一个openCV实现的性能参数 不能符合呢你的要求 并且你的系统有一个英伟达 CUDA GPU 设备那就还有回旋余地. 你可以移植或编写你自己的影像卡算法。

这个教程会让你很好的掌握如何使用openCV的GPU模块完成编码. 首先确认你已经知道如何使用core,highgui和improc. 模块。所以, 我们的目标是:

第 1 段(可获 2 积分)
  • GPU计算和CPU计算相比有什么不同?
  • 为 峰值信噪比 和 影像相似度 创建GPU代码
  • 优化代码最大性能

源代码

你可以在 samples/cpp/tutorial_code/gpu/gpu-basics-similarity/gpu-basics-similarity文件夹中找到OpenCV代码库的源代码和视频文件或者是 从这里下载。完整的源代码很长(因为需要通过命令行参数和性能测量的应用程序的控制)。 因此,为了避免搞乱这些部分与那些你会发现这里只有功能本身。

第 2 段(可获 2 积分)

PSNR 返回一个浮点数表明两个输入值是否类似,数值在 30 到 50 之间,越高越好。

double getPSNR(const Mat& I1, const Mat& I2)
{
    Mat s1;
    absdiff(I1, I2, s1);       // |I1 - I2|
    s1.convertTo(s1, CV_32F);  // cannot make a square on 8 bits
    s1 = s1.mul(s1);           // |I1 - I2|^2

    Scalar s = sum(s1);         // sum elements per channel

    double sse = s.val[0] + s.val[1] + s.val[2]; // sum channels

    if( sse <= 1e-10) // for small values return zero
        return 0;
    else
    {
        double  mse =sse /(double)(I1.channels() * I1.total());
        double psnr = 10.0*log10((255*255)/mse);
        return psnr;
    }
}



double getPSNR_GPU_optimized(const Mat& I1, const Mat& I2, BufferPSNR& b)
{
    b.gI1.upload(I1);
    b.gI2.upload(I2);

    b.gI1.convertTo(b.t1, CV_32F);
    b.gI2.convertTo(b.t2, CV_32F);

    gpu::absdiff(b.t1.reshape(1), b.t2.reshape(1), b.gs);
    gpu::multiply(b.gs, b.gs, b.gs);

    double sse = gpu::sum(b.gs, b.buf)[0];

    if( sse <= 1e-10) // for small values return zero
        return 0;
    else
    {
        double mse = sse /(double)(I1.channels() * I1.total());
        double psnr = 10.0*log10((255*255)/mse);
        return psnr;
    }
}

struct BufferPSNR                                     // 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 buf;
};

double getPSNR_GPU(const Mat& I1, const Mat& I2)
{
    gpu::GpuMat gI1, gI2, gs, t1,t2;

    gI1.upload(I1);
    gI2.upload(I2);

    gI1.convertTo(t1, CV_32F);
    gI2.convertTo(t2, CV_32F);

    gpu::absdiff(t1.reshape(1), t2.reshape(1), gs);
    gpu::multiply(gs, gs, gs);

    Scalar s = gpu::sum(gs);
    double sse = s.val[0] + s.val[1] + s.val[2];

    if( sse <= 1e-10) // for small values return zero
        return 0;
    else
    {
        double  mse =sse /(double)(gI1.channels() * I1.total());
        double psnr = 10.0*log10((255*255)/mse);
        return psnr;
    }
}
第 3 段(可获 2 积分)

SSIM 返回图像的 MSSIM。这也是一个浮点数,值在 0 和 1 之间(越高越好),每个通道都会有一个对应的值。因此我们返回一个 Scalar 的OpenCV 数据结构:

Scalar getMSSIM( const Mat& i1, const Mat& i2)
{
    const double C1 = 6.5025, C2 = 58.5225;
    /***************************** INITS **********************************/
    int d     = CV_32F;

    Mat I1, I2;
    i1.convertTo(I1, d);           // cannot calculate on one byte large values
    i2.convertTo(I2, d);

    Mat I2_2   = I2.mul(I2);        // I2^2
    Mat I1_2   = I1.mul(I1);        // I1^2
    Mat I1_I2  = I1.mul(I2);        // I1 * I2

    /*************************** END INITS **********************************/

    Mat mu1, mu2;   // PRELIMINARY COMPUTING
    GaussianBlur(I1, mu1, Size(11, 11), 1.5);
    GaussianBlur(I2, mu2, Size(11, 11), 1.5);

    Mat mu1_2   =   mu1.mul(mu1);
    Mat mu2_2   =   mu2.mul(mu2);
    Mat mu1_mu2 =   mu1.mul(mu2);

    Mat sigma1_2, sigma2_2, sigma12;

    GaussianBlur(I1_2, sigma1_2, Size(11, 11), 1.5);
    sigma1_2 -= mu1_2;

    GaussianBlur(I2_2, sigma2_2, Size(11, 11), 1.5);
    sigma2_2 -= mu2_2;

    GaussianBlur(I1_I2, sigma12, Size(11, 11), 1.5);
    sigma12 -= mu1_mu2;

    ///////////////////////////////// FORMULA ////////////////////////////////
    Mat t1, t2, t3;

    t1 = 2 * mu1_mu2 + C1;
    t2 = 2 * sigma12 + C2;
    t3 = t1.mul(t2);              // t3 = ((2*mu1_mu2 + C1).*(2*sigma12 + C2))

    t1 = mu1_2 + mu2_2 + C1;
    t2 = sigma1_2 + sigma2_2 + C2;
    t1 = t1.mul(t2);               // t1 =((mu1_2 + mu2_2 + C1).*(sigma1_2 + sigma2_2 + C2))

    Mat ssim_map;
    divide(t3, t1, ssim_map);      // ssim_map =  t3./t1;

    Scalar mssim = mean( ssim_map ); // mssim = average of ssim map
    return mssim;
}

Scalar getMSSIM_GPU( const Mat& i1, const Mat& i2)
{
    const float C1 = 6.5025f, C2 = 58.5225f;
    /***************************** INITS **********************************/
    gpu::GpuMat gI1, gI2, gs1, tmp1,tmp2;

    gI1.upload(i1);
    gI2.upload(i2);

    gI1.convertTo(tmp1, CV_MAKE_TYPE(CV_32F, gI1.channels()));
    gI2.convertTo(tmp2, CV_MAKE_TYPE(CV_32F, gI2.channels()));

    vector<gpu::GpuMat> vI1, vI2;
    gpu::split(tmp1, vI1);
    gpu::split(tmp2, vI2);
    Scalar mssim;

    for( int i = 0; i < gI1.channels(); ++i )
    {
        gpu::GpuMat I2_2, I1_2, I1_I2;

        gpu::multiply(vI2[i], vI2[i], I2_2);        // I2^2
        gpu::multiply(vI1[i], vI1[i], I1_2);        // I1^2
        gpu::multiply(vI1[i], vI2[i], I1_I2);       // I1 * I2

        /*************************** END INITS **********************************/
        gpu::GpuMat mu1, mu2;   // PRELIMINARY COMPUTING
        gpu::GaussianBlur(vI1[i], mu1, Size(11, 11), 1.5);
        gpu::GaussianBlur(vI2[i], mu2, Size(11, 11), 1.5);

        gpu::GpuMat mu1_2, mu2_2, mu1_mu2;
        gpu::multiply(mu1, mu1, mu1_2);
        gpu::multiply(mu2, mu2, mu2_2);
        gpu::multiply(mu1, mu2, mu1_mu2);

        gpu::GpuMat sigma1_2, sigma2_2, sigma12;

        gpu::GaussianBlur(I1_2, sigma1_2, Size(11, 11), 1.5);
        gpu::subtract(sigma1_2, mu1_2, sigma1_2); // sigma1_2 -= mu1_2;

        gpu::GaussianBlur(I2_2, sigma2_2, Size(11, 11), 1.5);
        gpu::subtract(sigma2_2, mu2_2, sigma2_2); // sigma2_2 -= mu2_2;

        gpu::GaussianBlur(I1_I2, sigma12, Size(11, 11), 1.5);
        gpu::subtract(sigma12, mu1_mu2, sigma12); // sigma12 -= mu1_mu2;

        ///////////////////////////////// FORMULA ////////////////////////////////
        gpu::GpuMat t1, t2, t3;

        mu1_mu2.convertTo(t1, -1, 2, C1); // t1 = 2 * mu1_mu2 + C1;
        sigma12.convertTo(t2, -1, 2, C2); // t2 = 2 * sigma12 + C2;
        gpu::multiply(t1, t2, t3);        // t3 = ((2*mu1_mu2 + C1).*(2*sigma12 + C2))

        gpu::addWeighted(mu1_2, 1.0, mu2_2, 1.0, C1, t1);       // t1 = mu1_2 + mu2_2 + C1;
        gpu::addWeighted(sigma1_2, 1.0, sigma2_2, 1.0, C2, t2); // t2 = sigma1_2 + sigma2_2 + C2;
        gpu::multiply(t1, t2, t1);                              // t1 =((mu1_2 + mu2_2 + C1).*(sigma1_2 + sigma2_2 + C2))

        gpu::GpuMat ssim_map;
        gpu::divide(t3, t1, ssim_map);      // ssim_map =  t3./t1;

        Scalar s = gpu::sum(ssim_map);
        mssim.val[i] = s.val[0] / (ssim_map.rows * ssim_map.cols);

    }
    return mssim;
}
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<gpu::GpuMat> 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)
{
    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;

    gpu::GpuMat buf;

    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), buf, 1.5, 0, BORDER_DEFAULT, -1, stream);
        gpu::GaussianBlur(b.vI2[i], b.mu2, Size(11, 11), buf, 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), buf, 1.5, 0, BORDER_DEFAULT, -1, stream);
        gpu::subtract(b.sigma1_2, b.mu1_2, b.sigma1_2, gpu::GpuMat(), -1, 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), buf, 1.5, 0, BORDER_DEFAULT, -1, stream);
        gpu::subtract(b.sigma2_2, b.mu2_2, b.sigma2_2, gpu::GpuMat(), -1, stream);
        //b.sigma2_2 -= b.mu2_2;

        gpu::GaussianBlur(b.I1_I2, b.sigma12, Size(11, 11), buf, 1.5, 0, BORDER_DEFAULT, -1, stream);
        gpu::subtract(b.sigma12, b.mu1_mu2, b.sigma12, gpu::GpuMat(), -1, 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, 1, -1, stream); //b.t1 = 2 * b.mu1_mu2 + C1;
        gpu::add(b.t1, C1, b.t1, gpu::GpuMat(), -1, stream);
        gpu::multiply(b.sigma12, 2, b.t2, 1, -1, stream); //b.t2 = 2 * b.sigma12 + C2;
        gpu::add(b.t2, C2, b.t2, gpu::GpuMat(), -12, stream);

        gpu::multiply(b.t1, b.t2, b.t3, 1, -1, stream);     // t3 = ((2*mu1_mu2 + C1).*(2*sigma12 + C2))

        gpu::add(b.mu1_2, b.mu2_2, b.t1, gpu::GpuMat(), -1, stream);
        gpu::add(b.t1, C1, b.t1, gpu::GpuMat(), -1, stream);

        gpu::add(b.sigma1_2, b.sigma2_2, b.t2, gpu::GpuMat(), -1, stream);
        gpu::add(b.t2, C2, b.t2, gpu::GpuMat(), -1, stream);


        gpu::multiply(b.t1, b.t2, b.t1, 1, -1, stream);     // t1 =((mu1_2 + mu2_2 + C1).*(sigma1_2 + sigma2_2 + C2))
        gpu::divide(b.t3, b.t1, b.ssim_map, 1, -1, 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;
}
第 4 段(可获 2 积分)

如何去做? -  GPU

现在你可以看到,对于每个操作我们有三种类型的函数。 一个用于CPU,两个用于GPU。 我为GPU做了两个函数是想说明,简单的移植你的CPU到GPU实际上会使它更慢。 如果你想要一些性能增益,你需要记住一些规则,我将在后面详细说明。

GPU模块的开发尽可能多地类似于对应的CPU。 这是为了使移植更容易。 在编写任何代码之前需要做的第一件事是将GPU模块链接到您的项目,并包含模块的头文件。 GPU的所有函数和数据结构都在cv命名空间的gpu子命名空间中。 您可以通过使用use namespace关键字将其添加到默认值,或通过cv ::明确标记它以避免混淆。 我在后面会介绍。

第 5 段(可获 2 积分)
#include <opencv2/gpu/gpu.hpp>        // GPU structures and methods

GPU代表图形处理单元。 它最初构建的目标是为了呈现图形场景。 这些场景以某种方式建立在很多数据上。 然而,这些数据并不是以顺序方式彼此依赖,因此可以对它们进行并行处理。 由于这个原因,GPU将包含多个较小的处理单元。 这些都不是最先进的处理器而且与CPU相比他们很慢。 然而,它的力量在于它庞大的数量。 在过去几年中,已经有越来越多的人在非图形场景渲染中使用GPU进行大规模并行计算。 这产生了对图形处理单元(GPGPU)的通用计算。

第 6 段(可获 2 积分)

GPU有自己的内存。 当您使用OpenCV将硬盘驱动器中的数据读取到Mat对象中时,是发生在系统内存中的。 CPU可以直接操作内存(通过缓存),然而GPU不能。 GPU需要将他用于计算的信息从系统内存转移到自己的内存上。 这是通过上传过程完成的,需要更多的时间。 最后,结果将被下载回您的系统内存,供您的CPU查看并使用。 不建议将小函数移植到GPU,因为上传/下载时间将大于并行执行所节约的时间。

第 7 段(可获 2 积分)

Mat对象仅存储在系统内存(或CPU缓存)中。 为了在GPU中获得一个OpenCV矩阵,你需要使用与GPU对应的GpuMat对象。 它的工作方式类似于2D限制的Mat,唯一的限制是不能引用GPU函数(GPU与CPU不能混合引用)。 要将Mat对象上传到GPU,您需要在创建类的实例后调用upload函数。 要下载时,您可以通过对Mat对象赋值或使用下载 函数。

Mat I1;         // Main memory item - read image into with imread for example
gpu::GpuMat gI; // GPU matrix - for now empty
gI1.upload(I1); // Upload a data from the system memory to the GPU memory

I1 = gI1;       // Download, gI1.download(I1) will work too
第 8 段(可获 2 积分)

一旦你将数据上传到GPU内存中,你就可以调用OpenCV的GPU函数。大多数函数与CPU上的命名相同,区别在于它们只接受GpuMat 输入。你可以在文档中找到这些文件的完整列表:在线文档或在源代码附带的OpenCV参考手册中。

另一件需要记住的事是,并非所有的通道数都可以在GPU上做出高效的算法。一般来说,我发现GPU图像的输入图像需要是单通道的char类型或四个通道的float类型的输入图像。很抱歉不支持double类型。为函数传递其他类型的对象将抛出异常,并在输出错误消息。这些函数的文档里大都说明了接收的数据类型。如果你有一个三通道图像作为输入,你可以做两件事:添加一个新的通道(使用char元素)或分割图像并为每个图像调用函数。不建议使用第一个,因为你浪费了内存。

第 9 段(可获 2 积分)

对于一些函数,元素(相邻项)所在的位置并不重要,快速解决方案是将其重塑成单通道图像。 这适用于PSNR实现,其中对于absdiff 方法,邻居的值并不重要。 然而,这不适用于GaussianBlur,对于SSIM需要使用分割方法。 有了这些知识,你就可以写出一个可用的GPU程序(像我写的GPU程序那样),并运行它。 你会惊讶地发现,它可能会比你的CPU实现慢。

优化

第 10 段(可获 2 积分)

这样做的原因是你在内存分配和数据传输上消耗了大量的时间。 在GPU上代价太高了。 另一种优化的可能是在gpu::Stream的帮助下引入异步OpenCV GPU调用。

  1. GPU上的内存分配相当可观。 因此,如果可能尽可能少的分配新的内存。 如果你打算创建一个多次调用的函数,最好在第一次调用期间就为函数一次性分配所有的局部参数。 为此,您将创建一个包含所有要使用的局部变量的数据结构。 例如在PSNR的情况下,这些是:

    struct BufferPSNR                                     // Optimized GPU versions
      {   // 基于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将只重新分配一个新的调用。

  2. 避免不必要的函数数据传输。 一旦你使用GPU,任何小数据传输都很重要。 因此,如果可能的话,在现有对象上进行所有的计算(换句话说,不创建新的内存对象 - 出于上一点解释的原因)。 例如,虽然容易使用一行公式描述算术表达式,但是它执行起来却更慢。 例如在SSIM中,我需要计算:

    b.t1 = 2 * b.mu1_mu2 + C1;
    

    尽管上述调用将成功执行,但是可以观察到存在隐藏的数据传输。 在进行求和之前,它需要在某处存储乘法的结果。 因此,它将在后台创建一个局部矩阵,将其加上 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);
    
  3. 使用异步调用( gpu::Stream)。默认情况下,每当调用gpu函数时,它将等待调用完成,然后返回结果。但是,可以进行异步调用,这意味着它将调用操作执行,为算法进行昂贵的数据分配并立即返回。现在你可以调用另一个函数,如果你想这样做的话。对于MSSIM,这是一个小的优化。在我们的默认实现中,我们将图像分割成通道,然后为每个通道调用gpu函数。使用流可以实现并行化。通过使用流,我们可以在GPU已经执行给定方法时进行数据分配,上传操作。例如,我们需要上传两个图像。我们将这些图像依次排队,并调用处理它的函数。这些函数将等待上传完成,但当执行第二个函数时,可以直接使用原来的输出缓冲区分配。

    gpu::Stream stream;
    
    stream.enqueueConvert(b.gI1, b.t1, CV_32F);    // Upload
    
    gpu::split(b.t1, b.vI1, stream);              // Methods (pass the stream as final parameter).
    gpu::multiply(b.vI1[i], b.vI1[i], b.I1_2, stream);        // I1^2
    
第 11 段(可获 2 积分)

结果与结论

在配备低端NVidia GT220M的Intel P8700笔记本电脑CPU上,性能数字如下:

Time of PSNR CPU (averaged for 10 runs): 41.4122 milliseconds. With result of: 19.2506
Time of PSNR GPU (averaged for 10 runs): 158.977 milliseconds. With result of: 19.2506
Initial call GPU optimized:              31.3418 milliseconds. With result of: 19.2506
Time of PSNR GPU OPTIMIZED ( / 10 runs): 24.8171 milliseconds. With result of: 19.2506

Time of MSSIM CPU (averaged for 10 runs): 484.343 milliseconds. With result of B0.890964 G0.903845 R0.936934
Time of MSSIM GPU (averaged for 10 runs): 745.105 milliseconds. With result of B0.89922 G0.909051 R0.968223
Time of MSSIM GPU Initial Call            357.746 milliseconds. With result of B0.890964 G0.903845 R0.936934
Time of MSSIM GPU OPTIMIZED ( / 10 runs): 203.091 milliseconds. With result of B0.890964 G0.903845 R0.936934

在这两种情况下,与CPU实现相比,我们的性能提高了近100%。 这可能只是使您的应用程序工作所需的改进。 您可以在这里观看YouTube的运行时实例。

第 12 段(可获 2 积分)

文章评论