GPU 上的超高品质图像旋转
GPU上的超高品质频域图像旋转。
引言
在本文中,我们将探讨如何在图形处理单元(GPU)上进行频域图像旋转。旋转的质量令人惊叹。我们将旋转一张图像,然后旋转已旋转的图像,以此类推。自然,我们期望图像会损失质量,然而我们将看到这种退化非常小。您还将看到一些令人难以置信的现象,例如以 100fps 的速度将图像旋转 1000 分之一度,仍然能够注意到图像在亚像素级别上“蠕动”。我们将使用 .NET 进行开发,并以 NVIDIA GPU 为目标,因此将利用 CUDAfy.NET,这是一个用于 .NET 的 CUDA 包装器。
Lena - 尽管旋转了 0.01 度 1000 次,但看起来仍然不错。
背景
上个世纪末,三位同事聚集在一起,试图将他们在一个大型荷兰研究机构开发的一款相当不寻常的芯片商业化。这款芯片将成为世界上最快的浮点快速傅立叶变换(FFT)处理器——PowerFFT。这对您来说可能没有太多意义,但如果您从事雷达处理或医学成像等领域,那么它将是一件大事,尤其因为它仅消耗 3 瓦功率。傅立叶变换通常将时域或空域函数转换为频域函数。如果您对纯正弦波进行傅立叶变换,您将得到一个图,其中有一个单一的尖峰代表正弦波的频率。该公司名为 doubleBW,我与 Laurens、Wout 和 Peter 一起担任软件工程师,负责为这款奇特的芯片设计编程软件。我们制作的演示之一就是本文的主题——频域图像旋转器。虽然 PowerFFT 芯片成功生产,但商业方面并未真正起飞,公司随后转向新领域。然而,在过去几年里,欧洲航天局购买了知识产权,并着手创建了一个适合太空使用的版本。我作为顾问被聘请参与此事,这让我重新审视了我以前的工作,并决定尝试将旧的图像旋转器移植到 GPU。我将本文献给 Laurens、Wout、Peter 以及我在那里工作的 5 年里共事过的所有才华横溢的工程师和好伙计。
频域图像旋转
总的来说,在执行旋转时有三个主要阶段。在第一阶段,我们对图像的每一行执行正向离散傅立叶变换(DFT)。在本例中,我们有一张 512x512 的黑白图像,即对 512 个 512 点的 DFT。由于是 2 的幂,我们可以使用快速傅立叶变换,但 CUDA FFT 库可以处理任意长度的 DFT,并且似乎速度相同。完成后,我们将每个点乘以一个系数。这些系数是特定于所需旋转角度的,共有三组系数,每组系数的大小与图像相同,并特定于三个阶段之一。这些集合的名称是“twiddle vectors”或“twiddle factors”。我不确定为什么。在此之后,我们使用逆 DFT 或反向 DFT 短暂返回到空域。使用 CUDA DFT 库时,此操作要求结果通过乘以 1/Width = 1/512 进行缩放。
Lena - 第一阶段后看起来不太好。
在第二阶段,我们将执行与第一阶段相同的过程,但方向是垂直的。这使我们离所需结果更近了一步。
Lena - 看起来好多了,但有些地方还是不对。
第三个也是最后一个阶段与第一阶段相同,但与所有阶段一样,它使用自己的 twiddle vectors。Lena 以其完整的旋转后的荣耀形象出现(实际上不是,但这是一个适合所有年龄段的网站,所以我将留给您自行搜索完整未裁剪的图像)。实际上,您还会注意到图像的角落里充满了由于四舍五入而产生的明显垃圾——完整的图像信息仍然存在于此,如果您以与前进相同的量向后旋转,您将完全恢复原始图像。
Lena 在所有三个必需的阶段之后。
使用代码
您的 PC 上需要安装 NVIDIA CUDA 5.5 64 位。部署机器还需要 CUDA CUFFT 和 CUBLAS 库,这些库可以在执行文件夹中或在搜索路径上的某个位置访问。如果您没有 CUDA 或 CUDAfy.NET 的经验,强烈建议您查阅我关于该主题的其他文章。
在频域中旋转图像是一项相当密集的处理操作。它也非常适合 GPU。由于我们使用 .NET,CUDAfy.NET 是 NVIDIA CUDA 的便捷包装器。CUDA 是一种使用 NVIDIA GPU 进行计算(而不仅仅是图形)的方法。CUDAfy 简化了 .NET 应用程序中使用 CUDA 的过程。将在 GPU 上使用的方法和结构用 Cudafy
属性标记。
示例图像存储为资源。通过调用 GetBytes
来访问和显示它并提取原始数据。所有 GPU 功能都封装在 GPUWrapper
类中。在其构造函数中会发生一些有趣的事情。
public GPUWrapper(int deviceId)
{
// Check if we already have a serialized CUDAfy module.
var mod = CudafyModule.TryDeserialize(GetType().Name);
// If we do not have a serialized module or if the checksum
// of the module does not match the assembly we recreate.
if (mod == null || !mod.TryVerifyChecksums())
{
// We want to translate (cudafy) the specified types.
// The result is stored in a CUDAfy module.
mod = CudafyTranslator.Cudafy(typeof(TwiddleSettings),
typeof(GPUWrapper), typeof(TwiddleGeneration));
// Serialize the cudafy module to an xml file.
mod.Serialize(GetType().Name);
}
// Get the CUDA device with index deviceId.
_gpu = CudafyHost.GetDevice(eGPUType.Cuda, deviceId);
// Load the CUDAfy module.
_gpu.LoadModule(mod);
// Instantiate object for storing pointers to GPU memory.
_gdata = new GPUData(_gpu);
// Instantiate object for CUDA FFT and BLAS libraries.
_maths = new GPUMaths(_gpu);
}
最重要的部分是调用 CudafyTranslator.Cudafy(...)
的那一行。此例程将标记的 .NET 代码转换为 CUDA C,然后调用 NVIDIA C 编译器为 GPU 生成中间语言(称为 PTX)。所有这些反射信息和其他输出(即 PTX)都存储在 CUDAfy 模块对象中。TwiddleSettings
是一个简单的结构,用于保存 twiddle vectors 的参数。GPUWrapper
和 TwiddleGeneration
是具有一些用 Cudafy
属性标记的方法的类。这些方法被转换为 CUDA C 函数,我们稍后可以从主机调用它们。
下一个有趣的 M部分代码用于上传图像并初始化辅助类。CopyToDevice
和 CopyFromDevice
用于在主机(CPU/系统内存)和 GPU(全局)内存之间传输数据。Launch
调用 GPU 上的一个函数。我们传递给 Launch
的 Width
和 Height
等值指定了我们希望并行启动多少个块和线程。第一个参数是块数,第二个参数是每个块的线程数。
public void UploadImage(byte[] image, float angle)
{
// Initialize the helper classes.
_gdata.Set(Width, Height);
_maths.Set(Width, Height);
// Copy the image to the GPU and then launch function ConvertByteToComplex.
_gpu.CopyToDevice(image, _gdata.SourceImage);
_gpu.Launch(Width, Height).ConvertByteToComplex(
_gdata.SourceImage, _gdata.SourceImageCplx, Width, Height);
UpdateTwiddles(angle);
}
public void UpdateTwiddles(float angle)
{
// Set the struct with our required settings
TwiddleSettings ts = new TwiddleSettings()
{
angle = angle,
height = Height,
width = Width
};
// Launch function GenerateTwiddlesOnDevice to create the three sets of twiddle vectors.
_gpu.Launch(Width, Height).GenerateTwiddlesOnDevice(ts, _gdata.TwiddleBuffers[0],
_gdata.ComplexFBuffers[0], _gdata.TwiddleBuffers[2]);
// Launch function Transpose to transpose (corner turn) the second twiddle vector set.
_gpu.Launch(new dim3(Width / BLOCK_DIM, Height / BLOCK_DIM),
new dim3(BLOCK_DIM, BLOCK_DIM)).Transpose(_gdata.ComplexFBuffers[0],
_gdata.TwiddleBuffers[1], Width, Height);
}
实际的处理循环在 BackgroundWorker
中执行。这给 GPU 代码带来了一个小麻烦,因为它意味着我们在一个非 GPU 初始化线程上执行工作。可以将其视为与在非主 UI 线程中与 UI 交互时需要使用 Invoke
类似——项目也大量使用了它来从 BackgroundWorker
更新 UI。为了解决这个问题,我们需要在主线程上调用 EnabledMultithreading()
,然后在与 GPU 交互之前在子线程上调用 SetCurrentContext()
。以下是 Process
方法的代码。您可以看到 CUDA FFT 库、我们自己的 Multiply
GPU 函数和 CUDA 基本线性代数子程序(BLAS)库是如何结合使用的。我们实际上启动了 12 个 GPU 函数才能执行一次旋转——这暗示了该操作的密集程度。最后一行代码将新旋转的图像复制到源图像之上,因此下次调用 Process
时,我们将对先前旋转的图像执行旋转。这是测试频域图像旋转质量的好方法。
public void Process(eProcessType type = eProcessType.Full)
{
float scale = 1.0F / (float)Width;
// All the calls below start functions on the GPU.
// 1st pass - forward FFT, multiply with twiddle vectors, inverse FFT and scale the result.
_maths.fwdPlan.Execute(_gdata.SourceImageCplx, _gdata.ComplexFBuffers[0]);
_gpu.Launch(Width, Height).Multiply(_gdata.ComplexFBuffers[0],
_gdata.TwiddleBuffers[0], _gdata.ComplexFBuffers[1], Width, Height);
_maths.invPlan.Execute(_gdata.ComplexFBuffers[1], _gdata.ComplexFBuffers[2], true);
_maths.blas.SCAL(scale, _gdata.ComplexFBuffers[2]);
if (type != eProcessType.OnePass)
{
// 2nd pass - corner turned forward FFT, multiply
// with twiddle vectors, corner turned inverse FFT and scale.
_maths.fwdPlanT.Execute(_gdata.ComplexFBuffers[2], _gdata.ComplexFBuffers[0]);
_gpu.Launch(Width, Height).Multiply(_gdata.ComplexFBuffers[0],
_gdata.TwiddleBuffers[1], _gdata.ComplexFBuffers[1], Width, Height);
_maths.invPlanT.Execute(_gdata.ComplexFBuffers[1], _gdata.ComplexFBuffers[2], true);
_maths.blas.SCAL(scale, _gdata.ComplexFBuffers[2]);
}
if (type == eProcessType.Full)
{
// 3rd pass - forward FFT, multiply with twiddle vectors, inverse FFT and scale.
_maths.fwdPlan.Execute(_gdata.ComplexFBuffers[2], _gdata.ComplexFBuffers[0]);
_gpu.Launch(Width, Height).Multiply(_gdata.ComplexFBuffers[0],
_gdata.TwiddleBuffers[2], _gdata.ComplexFBuffers[1], Width, Height);
_maths.invPlan.Execute(_gdata.ComplexFBuffers[1], _gdata.ComplexFBuffers[2], true);
_maths.blas.SCAL(scale, _gdata.ComplexFBuffers[2]);
}
// Copy to source image location.
_gpu.CopyOnDevice(_gdata.ComplexFBuffers[2], _gdata.SourceImageCplx);
}
为了可视化图像,我们首先将图像转换为 8 位灰度,然后从 GPU 复制到主机。
public void DownloadImage(byte[] buffer)
{
CheckIsSet();
// Convert the complex float array to byte array.
_gpu.Launch(Width, Height).ConvertComplexToByte(
_gdata.SourceImageCplx, _gdata.ResultImage, Width, Height);
_gpu.CopyFromDevice(_gdata.ResultImage, buffer);
}
许可证
Cudafy.NET SDK 包含两个大型示例项目,其中包括光线追踪、涟漪效果和分形。许多示例在 CUDA 和 OpenCL 上都得到了完全支持。它是一种双重许可的软件库。LGPL 版本适用于开发专有或开源应用程序,前提是您能遵守 GNU LGPL 版本 2.1 中包含的条款和条件。访问 Cudafy 网站了解更多信息。如果您使用 LGPL 版本,我们恳请您考虑向 Harmony through Education 捐款。这个小型慈善机构正在帮助发展中国家的残疾儿童。请访问 Cudafy 网站的慈善页面阅读更多信息。
关注点
2002 年,原始的旋转器在 PowerFFT PCI 卡上能够达到 45fps,包括 PCI 和图形显示。2011 年的一台配备 NVIDIA Geforce GT540M 的笔记本电脑可以达到 90fps。在计算机领域,9 年是一个漫长的时期,所以 doubleBW PowerFFT 远远领先于时代,并且在速度与功耗比方面可能仍然优于最新的 GPU。这可能就是为什么该芯片在未来的 欧洲航天局任务中仍有可能进入轨道的原因。
您可以在 Hybrid DSP Systems 网站 或 CodePlex 网站上找到更多关于 CUDAfy.NET 的信息,获得支持并访问最新版本。
历史
- 首次发布。