使用 OpenGL 和着色器的图像处理

使用 OpenGL 和 GLUT,您可以通过利用系统 GPU 内部的强大功能来提高图像处理的速度。

多年来,视频游戏一直在充分利用 GPU。现在,即使是非图形产品(如 Matlab)也开始利用 GPU 的数字处理能力。您也可以这样做。

本文讨论了使用 OpenGL 着色器执行图像处理。图像是从使用 Video4Linux 2 (V4L2) 接口的设备获得的。使用显卡提供的强大功能来执行部分图像处理可以减轻 CPU 的负载,并可能提高吞吐量。本文介绍了 Glutcam 程序(我开发的)及其背后的各个部分。

Glutcam 程序从 V4L2 设备获取数据,并将其馈送到 OpenGL 以进行处理和显示。要使用它,您需要以下组件

  • Video4Linux2 (V4L2) 设备。

  • OpenGL 2.0(或更高版本)。

  • GLUT 或 FreeGLUT 库。

  • GLEW 库。

首先,让我们简要了解一下各个组件,然后再了解完整的 Glutcam 程序。

Video4Linux2 设备

Video4Linux2 是 Linux 和许多视频设备(包括调谐器和一些网络摄像头)之间的接口。(有些设备仍然使用较旧的 V4L1 驱动程序。)Bill Dirks 于 1998 年启动了 Video4Linux2 API。它在 2002 年的版本 2.5.46 中合并到内核中。V4L2 API 控制成像设备。V4L2 允许应用程序打开设备,查询其功能,设置捕获参数并协商输出格式和方法。其中两件事对我们尤其重要:输出机制和输出格式。

输出机制将视频数据从成像设备移动到您的应用程序中。这比听起来要复杂,因为驱动程序在内核空间中拥有数据,而您的应用程序在用户空间中。有两种方法可以进行这种传输。它们是“读取”接口(即从设备读取的普通 read() 函数)和“流式传输”接口。

由于速度很重要,因此首选流式传输接口,因为它比读取接口快得多。读取接口较慢,因为它需要将所有数据从内核空间复制到用户空间。另一方面,流式传输接口可以通过两种方式之一消除这种复制:接口可以允许用户空间应用程序访问驱动程序中的缓冲区(称为内存映射),或者应用程序可以允许驱动程序访问用户空间中分配的缓冲区(称为用户指针 I/O)。这节省了每次图像可用时复制图像的开销。流式传输接口的缺点是复杂性:您必须管理一组缓冲区,而不是像通过读取函数那样每次都从同一缓冲区读取。

但是,这种复杂性是值得的。我在开发 Glutcam 时获得的最大速度提升是从读取切换到流式传输。

第二个需要注意的是输出格式。并非所有设备都可以原生提供所有输出格式。有些设备使用内核内软件在设备提供的格式和应用程序要求的格式之间进行转换。当设备可以提供的格式与应用程序要求的格式之间存在差异时,必须进行转换。内核驱动程序会进行转换并占用一些 CPU 时间。策略是将尽可能多的处理转移到 GPU 上。因此,请选择一种可以最大限度地减少内核内处理的格式。

主要格式为灰度、RGB 和 YUV。灰度和 RGB 大家都很熟悉。YUV(有时称为 YCbCr)将每个像素的亮度信息(也称为亮度,符号为 Y)与颜色信息(称为色度,符号为 U 和 V)分开。YUV 的发展源于早期黑白电视开始包含彩色,但仍然必须与黑白接收器兼容。YUV 类别中存在一个完整的格式系列,具体取决于颜色的存储方式。例如,人眼对颜色的敏感度低于对像素亮度的敏感度。这意味着每个像素都可以有一个亮度,并且颜色值可以在相邻像素之间共享——这是一种自然的压缩形式。常用术语包括以下内容

  • YUV444(每个像素都有 Y、U 和 V)。

  • YUV422(每个像素都有 Y,U 和 V 在相邻的水平像素之间共享)。

  • YUV420(每个像素都有 Y,U 和 V 在相邻的水平和垂直像素之间共享)。

信息在内存中的存储方式有很多格式变化。其中一种变化是组件是“平面”还是“打包”。在平面格式中,所有 Y 都存储在一起,然后是 U 或 V 块。打包格式将组件混合存储在一起。

Glutcam 接受 RGB、灰度、平面 YUV420 和打包 YUV422。有些源仅提供 JPEG;Glutcam 不支持 JPEG。

OpenGL

V4L2 涵盖了我们如何获取数据;为了显示数据,我们使用 OpenGL。OpenGL 是 SGI 于 1992 年首次发布的实时图形库。Brian Paul 于 1995 年发布了 OpenGL API 的 Mesa 实现。Mesa 允许 OpenGL 完全在软件中运行。SGI 于 2000 年为 OpenGL 示例实现提供了开源许可证。您的机器上可能已经安装了 Mesa 提供的 OpenGL,或者您可以从显卡供应商处获得它。供应商经常发布 OpenGL 库,这些库利用其图形硬件运行速度比纯软件版本更快。2006 年,API 的控制权移交给了非营利性技术联盟 Khronos Group。如今,您可以在从超级计算机到手机的各种设备中找到 OpenGL。

OpenGL 包含两个处理路径:片段处理路径和像素处理路径。片段处理路径是最广为人知的路径。您输入有关灯光、颜色、几何形状和纹理的信息,然后 OpenGL 生成图像。第二条路径是像素处理路径。像素处理路径从像素和纹理开始,让您可以对这些像素和纹理进行操作以生成图像。OpenGL 的一个可选部分称为 ARB 图像子集,它将一些常见的图像处理操作构建到 OpenGL 中。即使没有图像子集,也内置了大量用于缩放、扭曲、旋转、组合图像和叠加的操作。OpenGL 还包含用 GLSL 着色器语言编写的着色器的编译器和运行时环境。这使我们进入了下一个部分,着色器。

着色器是一个小程序,它(理想情况下完全)在 GPU 上运行。着色器可以用多种语言编写。Microsoft 有高级着色语言 (HLSL)。2002 年,NVIDIA 发布了 Cg(图形 C 语言)。OpenGL 有 GLSL(OpenGL 着色语言)。着色器可以回答以下两个问题之一

  1. 我在哪里绘制?(顶点着色器。)

  2. 图像的这部分是什么颜色?(片段着色器。)

本文重点介绍 GLSL 片段着色器。

着色器程序允许程序员修改 OpenGL 的“固定功能管线”。片段着色器从 OpenGL 纹理和着色器变量获取输入(图 1)。着色器具有类似于 C 语言的语法,具有相同的基本类型和用户定义的结构。

图 1. 片段着色器程序

GLSL 片段着色器看起来像这样

void main(void)
{
    // Making an assignment to gl_FragColor sets the color of
    // that part of the image. This function assigns the color to be
    // full red at full opacity.  The arguments to vec4 are
    // the components of the output color (red, green, blue, alpha).
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

GLSL 从 C 语言开始,然后添加了对向量和访问纹理的类型的支持。纹理使用“Sampler”类型读取。由于 Glutcam 使用二维数组,我们将使用 Sampler2D 类型。

将功能放入 OpenGL 和着色器有许多优点,包括

  • 显卡(通常)比 CPU 便宜,并且其性能的提升速度比 CPU 的性能提升速度更快。

  • 着色器可以在提高吞吐量的同时,将 CPU 保留用于其他任务。

  • 您无需编写代码来插值像素之间的值。OpenGL 纹理机制将为您进行插值。如果您的输出图像与输入图像的大小不同,这一点很重要。

许多图像处理算法都符合这种模式

for y in height
    for x in width
        pixel(x, y) = something(input_image, x, y, ...);
    end
end

这与 OpenGL 和着色器机制非常吻合(见下文)。

在着色器方法中提出问题时,input_image 是一个纹理。OpenGL 和着色器机制会为您处理循环。您的片段着色器只执行“something(input_image, x, y, ...)”部分。但是请注意,循环的迭代不能相互通信。有了这个限制,循环的迭代可以并行完成:从概念上讲,您的程序有一个实例为输出中的每个像素运行(图 2)。

图 2. 多个并行片段着色器

这有优点,但也有局限性。您的 GPU 是有限的资源,GLSL 也有局限性。这些限制包括

  • 数组只能是一维的:二维或更多维的数组必须展开。

  • 没有位域或位级操作。(OpenGL 3 放宽了这一限制。)

  • 没有指针。

  • 没有扩展精度(您的 GPU 可能支持 64 位或 128 位像素,但在依赖它之前请先检查)。

  • 没有打印语句。您的程序的唯一输出是该像素的颜色。

  • 数据必须适合卡上。如果您有非常大的纹理,则可能需要将它们分割并串行处理,或者将它们分布在多个 GPU 上。

  • 您的程序必须在允许的时间内运行。如果您的程序运行速度太慢,您的输出帧率将下降。

  • 并非所有输入组合都经过硬件加速。尽量保持在硬件加速路径上。

如果您的程序不符合这些限制,则要么编译失败,要么编译成功并在 CPU 上部分运行,而不是完全在 GPU 上运行。

如果您的程序不符合这些参数,请尝试相关方法,如 NVIDIA 的 CUDA 或新出现的开放计算语言 (OpenCL)。通用图形处理器单元 (GPGPU) 页面显示了一些可用的工具和技术。

OpenGL Utility Toolkit Library (GLUT) 或 FreeGLUT

OpenGL 本身是一个仅显示系统。它不处理窗口创建或放置,也不处理键盘和鼠标事件。您的 X 服务器的 GLX 层处理这些(有关更多详细信息,请参见 glXIntro 的手册页)。

GLUT 封装了所有这些功能,您将代码组织成回调,用于绘制窗口和处理键盘和鼠标事件等操作。GLUT 库处理所有较低级别的函数。它还为您提供了一种定义菜单的简便方法。除了 GLUT 之外,还有限制较少的实现 FreeGLUT(我在这里将它们都称为 GLUT)。Glutcam 使用 FreeGLUT 来处理所有这些细节。

GLUT 是较简单的 OpenGL GUI 工具之一,但它肯定不是唯一的工具。请查看 OpenGL 网站上编码资源下的选择。

OpenGL Extension Wrangler Library (GLEW)

由于多个组支持 OpenGL,因此新功能不会在所有实现中同时添加。大多数新功能最初都是由 OpenGL 架构审查委员会成员提出的扩展。随着这些扩展被检查和试用,一些扩展被更多供应商接受。一些扩展最终进入了核心 OpenGL 规范。OpenGL 扩展机制允许您的程序在运行时检查您的 OpenGL 库支持哪些功能以及这些例程位于何处。GLEW 库处理所有这些。GLEW 还提供 glewinfo 程序,该程序将告诉您有关您的 OpenGL 实现及其支持的功能。

Glutcam

现在,我们来了解 Glutcam 程序。Glutcam 将 V4L2 设备连接到 OpenGL 窗口,并使用 GLSL 片段着色器来处理图像(图 3)。理想情况下,我们希望通过使用 GPU 进行所有像素级操作来节省 CPU。至少,这就是将摄像机格式像素转换为 RGB 以进行显示。因为我想做的不仅仅是颜色空间转换,所以 Glutcam 还执行边缘检测。边缘检测是经典的图像处理操作。您可以使用菜单选择是在 CPU 还是 GPU 上完成边缘检测。您可以通过查看 Glutcam 的帧率并使用top命令来查看 CPU 负载的差异。

图 3. 从相机到屏幕的数据路径

在最佳情况下,您会告诉成像设备以着色器知道如何处理的格式生成图像,然后将图像数据放入一个或多个 OpenGL 纹理中。当您绘制时,数据将传递到您的片段着色器。如果您选择了无边缘检测或基于 CPU 的边缘检测,则着色器会将其转换为 RGB 以进行显示。如果您选择了基于 GPU 的边缘检测,则着色器会在图像上运行边缘检测算法并显示结果。

Glutcam 使用拉普拉斯边缘检测。CPU 选项将其实现为与内核的卷积

-1  -1  -1
    -1   8  -1
    -1  -1  -1

也就是说,您通过获取对应输入像素的值,将其乘以 8,然后减去相邻像素的值来计算每个像素的输出值。在像素的值与其邻居的值相同的位置,这会使输出像素为 0(黑色)。因此,边缘(颜色突然变化的地方)会显示出来。颜色变化缓慢的地方会变暗。内核和卷积是图像处理中的主力军。您可以使用相同的算法,只需更改内核中的值即可获得许多效果,例如锐化、模糊和平滑。

在着色器中,这采取从输入纹理中提取值、将其相乘,然后减去相邻纹素(纹理元素)的值,并将结果用作输出值的形式。它看起来更像代数,但效果相同。

要编译 Glutcam,请确保您已安装 FreeGLUT/GLUT、GLEW、OpenGL 2.0、V4L2(而非 V4L1)以及所有开发包。解压源文件,并查看 Makefile 中的说明。源文件附加在本文中。请参阅页面底部的文件。

构建完成后,使用以下命令调用它

glutcam [-d devicefile][-w width][-h height] \
        [-e LUMA | YUV420 | YUV422 | RGB]

如果未指定选项调用,Glutcam 将显示 320x230 灰度测试图案。

要连接到 /dev/video1 上的 V4L2 设备并获取 YUV420 格式的 640x480 图像,请使用

glutcam -d /dev/video1 -w 640 -h 480 -e YUV420

启动时,程序会告诉您有关其环境的信息——即,它正在与之通信的 V4L2 设备以及它正在使用的 OpenGL 来显示数据。您应该会看到类似于列表 1 中所示的启动信息。

对于您的机器,您必须弄清楚您的源使用哪个设备文件(例如,-d /dev/videoN),它喜欢生成什么尺寸的图像(-w、-h)以及它生成什么格式(-e)。

在 Glutcam 窗口中按下一个键以开始显示您的网络摄像头看到的内容。图 4 显示了一个示例。

图 4. 屏幕视图

如果您有图像子集,则可以使用切换直方图选项来打开或关闭直方图(图 5)。直方图显示了首次传递到 OpenGL 时每个亮度值的像素比例。基于 CPU 的边缘检测会影响直方图(因为它会更改着色器的输入),但基于着色器的边缘检测不会。

图 5. 带直方图的屏幕视图

右键单击窗口以调出菜单(图 6)。您可以选择一种图像处理算法来查看边缘检测的工作原理(图 7)。尝试边缘检测的两种选择(CPU 和着色器),并在top命令中查看帧率和 CPU 使用率的结果。

当 CPU 必须接触每个像素时(在执行直方图或在 CPU 上进行边缘检测时),帧率会显着下降(下降 50-70%),并且 CPU 使用率会显着上升(上升 100-200%)。但是,不要从这些结果中概括太多。硬件和 OpenGL 实现都会有所不同。回想一下,纯软件实现的 OpenGL 会更慢。Glutcam 将在 Mesa 7.6-01 上运行;因此您将看到输出,但您可能看不到帧率。

图 6. Glutcam 边缘检测菜单选项

图 7. Glutcam 边缘检测

最后,这里有一些着色器开发技巧(和迷信)

  • 使用top命令来密切关注程序的 CPU 使用率。

  • 从简单的着色器开始,然后逐步增加复杂性。当您的帧率下降或 CPU 使用率飙升时,请重新考虑您的方法。这可能意味着您正在 CPU 而不是 GPU 上运行。

  • 观看 OpenGL 在编译着色器时的编译时间日志。警告级别和错误消息的质量可能会有很大差异。注意警告,表明您的着色器部分在 CPU 上运行。

  • 观察您的代码进行的纹理访问次数。在我的测试中最旧的机器上,用于边缘检测的 3x3 矩阵是我可以在不降低帧率的情况下使用的最大矩阵。在较新的机器上,使用 5x5 矩阵的操作效果良好。

  • 在深入研究之前,先分析应用程序的 CPU 部分。您希望 CPU 将工作交给 GPU,并且 GPU 有足够的时间来完成工作。因此,如果您加快 CPU 部分的速度,则可能会为 GPU 提供更大的工作时间增量。如果您的目标是 60Hz,则您希望 GPU 上有 16 毫秒的工作,并且有 16 毫秒的时间来完成它。

  • 使用存储在内存中的测试图案来查看您的瓶颈是获取图像还是在获得图像后处理图像。

  • 如果您已将监视器的刷新率与 OpenGL 缓冲区交换同步,请查看降低监视器的刷新率是否会提高您的帧率。使用xrandr命令来显示可用于您的分辨率和刷新率。

  • 最大限度地减少您对传递给着色器的 OpenGL 变量进行的状态更改次数。这些必须像图像数据一样传递到您的 GPU。

  • 使用xmag命令来查看屏幕上像素的数值。

  • 尝试不同的屏幕分辨率和 OpenGL 窗口大小。有些可能会加速,而另一些则不会。

  • 使用直方图时,箱数较少的直方图比箱数较多的直方图减慢过程的速度要慢。

  • 请注意,您的输入源可能具有相关的速率。例如,pwc 驱动程序模块具有“fps”参数。

  • 简化着色器中的数学运算。

  • GLSL 编译器会积极优化。这包括优化掉着色器中它确定不必要的部分。密切关注您尝试设置着色器变量值时收到的警告。如果您收到错误,则该变量可能已被优化掉。如果所有变量都给您错误,则整个着色器可能未能编译。

  • 编写您的着色器,使其易于 GLSL 编译器进行优化。例如,硬编码循环边界而不是从 OpenGL 传入它们可以加快着色器的速度。

  • 如果您使用的是打包格式,请注意纹理插值。如果您有平面格式,则在两个相邻值(例如两个亮度)之间进行插值会为您提供合法值。在亮度和相邻色度之间进行插值不会给您带来您想要的结果。

  • 密切关注着色器开发和性能分析工具。管道中有些有趣的东西。

结论

我在 1990 年代的 Usenet 新闻组中看到了一个问题(当时 486 是最先进的)。发帖者想获得一个协处理器来加速光线追踪。任何人能提出的最佳建议是重新编程声卡上的数字信号处理器。使用声卡来获得更好的图形效果的想法让我感到有趣。我们现在正处于一个开始成为这种情况的镜像的时代。借助可编程显卡和 CUDA 和 OpenCL 等相关项目,您可以使用图形硬件来加速其他计算。

George Koharchik 愉快地感谢审阅本文的人员:Mike Menefee、Rick Still、Jim King 和 Candy Koharchik。可以通过 g-koharchik@raytheon.com 联系 George。

加载 Disqus 评论