通常简单地将 CPU 移植到 GPU 实际上会使其变慢。如果您想要提高性能,则需要记住一些规则,稍后我将详细介绍。
GPU 模块的开发尽可能地与其 CPU 对应模块相似。这使得移植过程更加容易。在编写任何代码之前,您需要做的第一件事是将 GPU 模块链接到您的项目,并包含模块的头文件。GPU 的所有函数和数据结构都在cv命名空间的gpu子命名空间中。您可以通过use namespace关键字将其添加到默认命名空间,或者通过 cv:: 明确地将其标记在任何地方以避免混淆。我稍后会做。
#include <opencv2/gpu.hpp> // GPU 结构和方法
GPU 代表“图形处理单元”。它最初是为渲染图形场景而构建的。这些场景以某种方式建立在大量数据之上。然而,这些场景并非都以顺序方式相互依赖,而是可以并行处理它们。因此,GPU 将包含多个较小的处理单元。这些不是最先进的处理器,在与 CPU 进行一对一测试时,它会落后。然而,它的优势在于它的数量。近年来,在非图形场景中利用 GPU 的这些巨大并行能力的趋势日益增加;渲染也是如此。这催生了图形处理单元上的通用计算 (GPGPU)。
GPU 有自己的内存。当您使用 OpenCV 将硬盘中的数据读取到系统内存中的Mat对象中时。CPU 以某种方式直接在此基础上工作(通过其缓存),但 GPU 不能。它必须将计算所需的信息从系统内存传输到自己的内存中。这是通过上传过程完成的,非常耗时。最后,结果必须下载回您的系统内存,以便您的 CPU 查看和使用它。不建议将小函数移植到 GPU,因为上传/下载时间将大于您通过并行执行获得的时间。
Mat 对象仅存储在系统内存(或 CPU 缓存)中。要将 OpenCV 矩阵传输到 GPU,您需要使用其 GPU 对应项cv::cuda::GpuMat。它的工作原理类似于 Mat,但仅限于 2D,并且其函数没有返回引用(不能将 GPU 引用与 CPU 引用混合)。要将 Mat 对象上传到 GPU,您需要在创建类的实例后调用上传函数。要下载,您可以使用对 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
将数据存入 GPU 内存后,您可以调用 OpenCV 的 GPU 启用函数。大多数函数的名称与 CPU 上的名称相同,不同之处在于它们仅接受GpuMat输入。
要记住的另一件事是,并非所有通道号都可以在 GPU 上制作有效的算法。通常,我发现 GPU 图像的输入图像需要是一通道或四通道图像,并且项目大小需要是 char 或 float 类型之一。抱歉,GPU 不支持双重。为某些函数传递其他类型的对象将导致抛出异常,并在错误输出中显示错误消息。文档在大多数地方详细说明了输入所接受的类型。如果您有三个通道图像作为输入,您可以做两件事:添加新通道(并使用 char 元素)或拆分图像并为每个图像调用函数。第一个并不推荐,因为这会浪费内存。
对于某些函数,元素(相邻项)的位置无关紧要,快速解决方案是将其重塑为单通道图像。这是 PSNR 实现的情况,对于absdiff方法,邻居的值并不重要。但是,对于GaussianBlur,这不是一个选项,因此需要对 SSIM 使用分割方法。有了这些知识,您可以编写一个 GPU 可行代码(例如我的 GPU 代码)并运行它。您会惊讶地发现它可能比您的 CPU 实现慢。
优化
这样做的原因是您将内存分配和数据传输的代价抛在了一边。而在 GPU 上,这个代价实在是太高了。另一种优化的可能性是借助 cv ::cuda::Stream引入异步 OpenCV GPU 调用。
- GPU 上的内存分配相当可观。因此,如果可能的话,请尽可能少地分配新内存。如果您创建一个打算多次调用的函数,最好在第一次调用期间只为该函数分配一次任何本地参数。为此,您需要创建一个包含您将使用的所有本地变量的数据结构。例如,在 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;
};
然后在主程序中创建它的一个实例:BufferPSNR bufferPSNR;
最后,每次调用时将其传递给函数:
double getPSNR_GPU_optimized(const Mat& I1, const Mat& I2, BufferPSNR& b)
- 现在,您可以访问这些本地参数:b.gI1、b.buf等。如果新矩阵大小与前一个矩阵大小不同,GpuMat 只会在新调用时重新分配自身。
- 避免不必要的函数数据传输。一旦进入 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::添加(b.t1,C1,b.t1);
使用异步调用(cv::cuda::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
结果与结论
在英特尔 P8700 笔记本电脑 CPU 与低端 NVIDIA GT220M 搭配使用时,性能数据如下:
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%。这可能正是您的应用程序运行所需的改进。
2万+

被折叠的 条评论
为什么被折叠?



