2018 年 10 月

33 卷,第 10

此文章由机器翻译

GPU 编程的人脸检测在 GPU 上使用 Eigenfaces 算法

通过Kishore Mulchandani

计算机视觉的涉及标识的计算机科学领域或标记的图像中的区域。映像是可以并行进行运算的像素为单位的大型的二维数组。Gpu 非常适合该前期步骤包括大量的数据并行性,一种类型的数据集的不同部分中的并行度可以同时在多个处理器上运算时加快算法的实现。因此,它应该可以将 Gpu 应用到实现效率的计算机视觉算法的任务。在本文中我将使用 GPU 来实现面部检测中计算机视觉的热门问题。

人脸检测,顾名思义,发现对应于人类的人脸的照片或图像中的区域。这是处理应用筛选器或编辑,以在图像中人脸的应用程序的重要先决条件。自动模糊、 屏蔽、 突出显示和增强是需要知道哪些像素应修改以完成该操作的示例。

有几种算法当前正在使用以检测人脸,但在本文中,我将利用 eigenfaces 方法,其中一个最早发布的人脸检测方法。它也是计算密集型操作。我的目标是演示使用 Gpu 加速计算要求苛刻的算法,并在 GPU 上显示的高级编程简介。

本文假设读者熟悉类似 c 语言的语言和线性代数和图像处理的一般概念。

Eigenfaces

根据线性代数您知道,是否有 n 维点的集合,您可以找到一些主要组件或本征向量,为此集合。这些本征向量还构成这些点的空间的基础。最高重要性的 eigenvector 是具有最大 eigenvalue。属于此集合的任何新点应最重要 eigenvector 还具有更高版本的投影值。

通过人脸集合转换为 n 维点继续 eigenfaces 方法。这是将每个二维图像转换成一维向量。整个集合被打包为大型矩阵,其中每个列是一维向量。通过将此与它的转置矩阵乘以形成矩形矩阵找到本征向量和本征值。本征向量集合的点的发现也被称为主体组件分析,并在特征向量被称为主体组件。

任何人脸检测算法的起始点是一个好的数据库的人脸。有几个可在 Internet 上,但本文中我所做的公开可用"标记为人脸在通配符"使用集合 (vis www.cs.umass.edu/lfw)。派生的集合转换为灰度是可在下载conradsanderson.id.au/lfwcrop ,还提供本文随附的下载。此集合包含的人脸 13,233 64 x 64 灰度图像。图 1显示映像的子集。请注意,所有映像具有已裁剪,使都之包括仅人脸。

中的通配符的数据集标记为人脸中的前 100 个映像
图 1 中的通配符的数据集标记为人脸的前 100 个的映像

已打包的矩阵,矩阵中的每个列处于集中的数据,1-D 人脸图像所示图 2。请注意,级别很高强度沿此图像的行匹配。

缩放包含 4,096 行和 13,233 列大矩阵表示形式
图 2 扩展具有 4,096 行和 13,233 列大的矩阵表示形式

找到的本征向量和本征值后,最重要 eigenvector 时考虑为二维图像看起来像在模糊人脸图 3

大矩阵的第一个 Eigenface
图 3 大矩阵的第一个 Eigenface

一旦计算完第一个 eigenface 下, 一步是搜索与 eigenface 投影在达到较大的值的区域的测试映像。此较大的值指示测试图像区域和 eigenface 彼此靠近的人脸的 n 维空间。搜索区域将为 64 x 64 像素区域开始测试映像的每个像素。每个搜索是搜索的独立于在其他起始像素,这意味着没有高级别的数据并行度。

如前文所述,Gpu 擅长于加速并行计算。但这并不意味着应在 GPU 上实现的每个算法。系统体系结构仍有系统内存和 GPU 内存之间的相对较慢连接。需要处理此数据的数据必须经过 PCI Express 总线,它甚至是在 PCI-e 球道每第 4 代可以将仅高达 32GT/秒,相对更慢传输的速度比的 CPU 和系统的内存之间。这意味着不能容忍延迟较高的应用程序不会有效地运行在 GPU 上。数据集的大小必须足够大,以调整将从系统内存的数据复制到 GPU 内存的开销并备份。如果应用程序必须根据数据大小的显著大型工作负荷,若要执行的操作是在本质上,类似,并执行任务所需的所有数据中的 GPU 内存可都容纳 GPU 是应用程序的理想的处理器。我将介绍,在人脸检测的情况下 GPU 非常适合于处理的一部分。

人脸检测算法有两个不同阶段。第一阶段包括创建一系列人脸 eigenface 矩阵。此集合通常不会更改,因为 eigenface 矩阵计算的计算需要在一次每个集合。第二个阶段是投影阶段,在其中测试区域被投影到第一个 eigenface 有多大投影。此阶段通常会经常发生这种情况。例如,在安全相机应用程序这一阶段可能会出现在 30 帧 / 秒,生成大量的数据。因为即使成本最低的网络摄像机如今支持 HD 分辨率,图像的分辨率也可能是非常大。另外,人脸检测程序通常运行的其他图形操作,如模糊或突出显示,和人脸检测的结果已使用在 GPU 上的程序的另一部分。

因此,第一阶段不是 GPU 实现中,提供它需要两个阶段是 GPU 实现的很好的匹配项,因为它满足所有条件时要执行低频率的良好候选项。

以下各节中我将介绍您如何执行阶段的人脸检测,两个,但首先我在上如何编程 GPU 不同于在 CPU 上展开。

编程 GPU

GPU 编程范例却大不相同的 CPU。于 OS GPU 显示为一台设备,并且其安装的驱动程序管理此设备上工作的执行。OS 和 GPU 驱动程序协同工作以帮助完成的任务。最低级别的命令序列发送到通过其命令队列的 GPU 设备。这些命令可能涉及设置状态,CPU 内存和 GPU 内存和正在运行的代码之间复制数据。Gpu 具有其自己指令的语言,与 CPU 语言大不相同。幸运的是,这是不需要了解除非你正在开发 GPU 编译器。大多数编程人员使用一个或多个 GPU 供应商在其驱动程序,例如 Direct3D、 OpenCL 和 OpenGL 中实现的 Api。这些 Api 在跨不同供应商和系列的体系结构提供可移植性和兼容性时隐藏低级别的复杂性。

本文中我已选择使用 OpenCL,所有主要 CPU 和 GPU 供应商支持的标准。OpenCL 已围绕自 2009 年以来已几个版本的最新版本 2.2,并且它将继续发展正在 (khronos.org/opencl)。非常重要的这一事实,要注意 OpenCL 是它可以指定目标不仅的 GPU,但也是 CPU。这是 OpenCL 很大 plus,因为它意味着您可使用的系统上的所有计算资源。出于此原因,OpenCL 称为并行编程的异类系统的 API。OpenCL 主机程序可以决定要使用哪些之前浏览的可用性和计算设备的功能。

幸运的是,编程模型和抽象是之间的各种 Api 非常相似。这意味着您应该能够将映射中不会出现问题的其他 api OpenCL 学到了的概念。让我们立即深入了解人脸检测的 OpenCL 实现。

设置环境

若要使用 OpenCL,除了在驱动程序中的运行时支持开发需要从相应的供应商的 SDK。请参阅本文随附下载数字版中的文档。

由于 OpenCL 是一种开放标准,任何供应商可以提供其自己版本的运行时为其硬件优化。

OpenCL 编程词汇包含平台、 设备和命令队列。一个平台是可用的系统上实现的同义词。设备是指一种支持的独立计划从其他设备的硬件。每台计算机具有至少一个 CPU 设备。大多数工作站或游戏类计算机会将列出的其他设备是分立的 GPU。高端工程工作站通常具有多个 Gpu,其中一个附加到其他人仅供计算的使用时显示。每个设备可以有多个程序将向其中插入命令的命令队列。

在主机上运行每个 OpenCL 程序开始通过发现在其运行的系统的功能。主机程序尝试确定在系统上可用的平台,然后枚举的设备。该程序专门请求设备类型,例如 CPU 设备类型或 GPU 设备类型。选择设备后创建一个上下文和想要使用的每个设备的命令队列。下面是一些可以执行上述操作 (起见,我已排除所需的调用的参数) 所做的调用:

clGetPlatformIDs(...); // Gets the ids of the platforms available
clGetDeviceIDs(...); // Gets the devices with certain capabilities
clCreateContext(...); // Creates a context on the device
clCreateCommandQueueWithProperties(...); // Creates a command queue with
                                         // certain properties

一旦命令队列设置,主机程序可以是只读的、 只写或读写在设备上创建的缓冲区。它们可以用于执行任何有意义的操作之前,必须使用系统内存中的数据初始化缓冲区:

clCreateBuffer(...);  // Creates a read only buffer on the device
clEnqueueWriteImage(...);  // Queues a buffer copy from the system to
                           // device memory

数据往往是较大的缓冲区或整数和浮点值的数组,通常以异步方式复制。在设备上同样创建计算结果的存储位置的缓冲区。OpenCL 具有名为映像的特殊数据类型。许多密集型任务通常源自映像或三维的数据集的形式在其中,这些特殊的数据类型派上用场。这些类型支持不同的采样操作,如最接近的示例或求平均值。

内核的程序,因为它们在 OpenCL 术语中,引用必须为编译和链接,命令队列中排队。我将详细描述这方面更高版本,如 CPU 编程人员可能想知道为什么在运行时编译内核:

clCreateProgramWithSource(...); // Create a program from kernel source
clBuildProgram(...); // The program is compiled; errors from compilation
                     // are reported
clCreateKernel(...); // The kernel is created from the program

一旦创建内核后,设置内核参数,并内核然后排队等待执行命令队列中:

clSetKernelArg(...); // Set the value of a particular argument
clEnqueueNDRangeKernel(...); // Queue the kernel

随附的源代码,一个执行投影 eigenface 上的区域中有两个内核和另一种查找本地高峰从投影的结果。投影操作是实质上是矢量点积。

点积的较大值意味着更多映像中的区域和 eigenface 之间的相似之处。这些较大值的峰值找不到或使用第二个内核。因为第二个内核才能使用它们,第一个内核的结果必须为可用,OpenCL 事件用于同步。使用以下 API 调用的一个或多个事件可以等待主机程序:

clWaitForEvents(...);

在设备上执行完这两个内核后, 第二个内核的结果将在设备缓冲区,然后复制回可以使用该进一步的主机内存中。

现在让我们看看一个在中启动的内核图 4。此类内核调用测试映像中的每个像素。它首先开始该像素的区域中查找 4,096 维向量 (64x64) 之和,然后减去从区域的每个像素之和的身份。它然后计算内核作为参数传递 eigenface 此区域的点积。

图 4 OpenCL 内核中测试映像的区域中创建 Eigenface 投影

__kernel void eigenFaceSearch(
    __read_only image2d_t testImage,        // Input test image
    __write_only image2d_t projectionImage, // Output projection image
    int rows,                               // Number of rows in the test image
    int cols,                               // Number of columns in the test image
    __constant float* eigenFace,            // The first eigenface buffer
    sampler_t sampler                       // Sampler for reading the image data
    )
  {
    int myCol = get_global_id(0);           // Column index of test image pixel
    int myRow = get_global_id(1);           // Row index of test image pixel
    float4 dotp = {0.0f, 0.0f, 0.0f, 0.0f}; // Resulting dot project variable
    float4 pixel = {0.0f, 0.0f, 0.0f, 0.0f};// Variable for reading the pixel
    float sum = 0.f;                        // Sum of the pixels in the test region
    float meanVal=0.f;
    float patch[4096];                      // Local buffer to hold the mean
                                            // subtracted region
    int2 myCoords;                          // Local variable for holding current
                                            // pixels coords
    myCoords.x = myCol;                    
    myCoords.y = myRow;
    if (!(( myCol > cols-64) || (myRow > rows - 64))) { // Bounds check
      for (int i = 0; i < 64; i++) { // Loop over every pixel in test region
        int2 coords;
        coords.y = myRow + i;
        for (int j = 0; j < 64; j++) {
          coords.x = myCol + j;
          // Read the pixel at coords, from the test image using the sampler
          pixel = read_imagef(testImage, sampler, coords); 
          patch[i*64+j] = pixel.x;  // Take the first component as it is a single
                                    // float buffer
          sum += patch[i*64+j];     // Accumulate the sum
        } 
  }  
    meanVal = sum / 256.;           // An intensity factor proportional to sum.
    for (int i= 0; i < 4096; i++){  // Remove from each pixel
      patch[i] -= meanVal;          // Per component subtract of intensity factor
    }
    // Perform the dot product
    for (int i= 0; i < 4096; i++){
      dotp.x += patch[i] * eigenFace[i]; // Do the component-wise dot product
    }
    // Write back the dot product into the projection Image
    write_imagef(projectionImage, myCoords, dotp);
  }
}

编译 OpenCL 内核

正如前面提到的则 OpenCL 编译器宿主应用程序调用在运行时。编译器与内核的整个源字符串形式提供。通常情况下,内核会非常快速、 编译而不是小型代码片段和应用程序是敏感到取回结果所需的每个额外毫秒,除非你可能不会关注它。但对于复杂的应用程序具有数百个内核,延迟可能会明显。幸运的是,中间语言 (IL) 选项存在,可以减少总体的编译时间。(IL 还有助于保护知识产权,如嵌入的字符串的内核代码可能会泄露专有信息。) 运行时编译的内核的好处是,它让应用程序恢复的弹性硬件中的更改。它是相当常见的是 PC 的用户可以在其计算机的生命期期间升级他们 Gpu。新的体系结构与新的 GPU 可能有一种全新的指令设置,保留预编译二进制文件从较旧 GPU 无法工作。

它拼接在一起

预计算 eigenface 图像,并将其作为包含单精度浮点数据按行优先顺序的二进制文件提供。测试图像也转换为浮点灰度图像。测试映像具有以下格式:

Height: 4 byte unsigned integer   
  // The number of rows in the image
Width: 4 byte unsigned integer    
  // The number of columns in the image
Intensity Buffer:   Height * width single precision floating point buffer
  // Data buffer

具有此类简单格式,它应该是简单地生成可供我的程序的测试映像。计算的结果也被保存为浮点型数据的缓冲区。  此外,生成的所有包含的人脸的矩形的左上角的坐标的文本文件。可以将此数据导入的任何程序。我在 MATLAB 中保存的使用情况数据 (bit.ly/2isajNJ)。

可以轻松地修改该程序,以 CPU 而不是 GPU 上运行。

运行的结果

我在多个测试映像上运行该程序。因为代码当前搜索匹配的区域只有 64 x 64,如果测试映像都具有大得多或较小的人脸,我不会希望它能够找到很好。但是,当测试图像具有大约具有相同的大小 eigenface,除非人脸被旋转,它应也执行操作。我的一个著名的映像上运行的结果所示图 5。使用不带第一个 eigenface 上区域的非正常化投影 eigenfaces 方法识别人脸。

拍摄采取的第五个 Solvay 大会上保存在 1927
图 5 照片拍摄的第五个 Solvay 大会上保存在 1927

我得到了很多假阳性和假负。缺少多个人脸,并已突出显示多个非人脸区域。其中一些是我用于投影图像中检测到高值 (峰值) 的本地最大值搜索方法的结果。我只需搜索邻域 97 x 97 像素,看是否我会找到更高的值。结果是突出显示多个区域而没有人脸方面存在但仍拥有一个更高版本比其本地邻域值。此方法可以得到改进以减少误报。 

请记住,结果的质量取决于其包括在强度等的图像分辨率、 光照条件和可变性的示例图像质量。

GPU 和 OpenCL。CPU

有多个并行处理技术 CPU 前面中,从 SIMD 矢量化,为显式的多线程处理,对编译器杂注中的循环 OpenMP 样式并行化。若要真正比较什么速度更快或更高的吞吐量将很难除非每个可能的技术组合中做了大量优化实现测试。也就是说,它是从一个设备类型切换到代码中的另一个给定的难易程度,我决定尝试一下。

我运行此程序时,在这两种设备类型,CPU 和 GPU,以及从两个不同"平台。" 我在运行此系统是游戏便携式计算机的 Intel Core i7-8750 H cpu @2.20 具有 16 GB RAM 与 NVIDIA GeForce GTX 1060 GPU GHz。我可以选择任何三个平台上的 CPU 设备或上一个来自 NVIDIA 的 GPU 设备类型。请注意,GPU 供应商可能提供 CPU 实现,连同其 GPU 实现,这种情况下,它就很难进行优化。

GPU 可喜的是,按预期方式执行与 CPU 版本采用 12 到 21 时间更长时间,如中所示图 6。分析不包括任何 I/O,但未包括的时间将从系统的缓冲区复制到设备内存。所有时间都均以毫秒为单位。

GPU 和 CPU 时间使用 OpenCL Eigenface 搜索实现的比较
图 6 GPU 和 CPU 时间使用 OpenCL Eigenface 搜索实现比较

内核是编写的意图以了解并不以获取最佳性能,因此采取这些比较持保留态度。实际上,内核的性能优化是一个复杂的过程,并在部署程序之前的一个重要步骤。流水线处理的缓冲区副本,减少寄存器中的内核数和保持一致内存访问是用于优化内核一些技术。

同一 OpenCL 主机程序可以同时为所有系统上可用的不同设备意味着您可以进一步提高吞吐量,通过采用所有这些设备的队列工作这一事实。如果您运行人脸检测上安全视频连续镜头,例如,无法将每个帧或 subframe 在其吞吐量成比例不同的设备队列之间。系统内存和系统总线仍共享,并可能成为瓶颈,因此,遗憾的是,您不能通过添加更多设备无限期地提高性能。

本征任何内容

尽管本文着重于检测的人脸,但不会防止这种方法应用于检测其他实体或对象。技巧就是收集大量测试映像,请勿一些预处理,以使其大小完全相同、 注册的一些突出功能的位置,然后计算本征向量。创建本征向量后, 搜索代码保持不变。因此,如果要开发代码来检测手钉子波兰语可视化应用程序,无法进行动手操作的数据库和图像的手可能生成的"eigenhands"和搜索。增强的现实应用程序呈上升趋势,这些类型的应用程序很可能会出现。

总结

深入探讨第一次的 GPU 开发的一名程序员,可能有陡峭的学习曲线,但回报可能很有必要。Gpu 具有大量的计算能力,可以利用使用 OpenCL 等编程语言。本文可作为同时查找人脸图像使用 eigenfaces 方法中的受欢迎的实际问题的 GPU 编程简介。这种方法可以扩展到其他形状和对象。


Kishore Mulchandani开发 3d 图形应用程序,并执行中的 Cpu 和 Gpu 上运行的并行代码的性能改进。高性能计算基准测试开发的图形,三维形状从使用计算机视觉的二维图像数据的重新构造和虚拟现实娱乐应用程序是一些他最新的关注和感兴趣的工作。他可以到达Kishore@vanishinglines.com

衷心感谢以下 Microsoft 技术专家对本文的审阅:James McCaffrey, Ravi Shankar Kolli


在 MSDN 杂志论坛讨论这篇文章