如何用 200 行 JavaScript 程式码实现人脸检测_pico

如何用 200 行 JavaScript 程式码实现人脸检测_pico

如何用 200 行 JavaScript 程式码实现人脸检测?_pico

在超市、地铁、车站等很多场景中,人脸识别已经被广泛应用,但是这个功能究竟是怎么实现的?

在本文中,将以 pico.js 库为例,分享实现轻量级人脸识别功能的具体开发过程 。

作者 tehnokv

译者 谭开朗,责编 屠敏

pico.js 是一个只有 200 行纯 Java 程式码的人脸检测库,具备实时检测功能(在实际环境中可达到200+ FPS),压缩后仅 2kB 。

开源代码地址:https://github.com/tehnokv/picojs;

简介

本文将介绍pico.js,这一由Java编写的用于人脸检测的程式码库,并展示其工作原理。尽管现已有类似的专案,但我们的目标是提供更小、计算效率更高的替代方案。

在深入考究其细节前,建议各位用计算机的网络摄像头体验一下人脸检测的实时演示(也适用于移动装置)。注意,所有程式都是在客户端完成的,即不向服务端传送影象。因此,各位无需担心在执行这段程式码时的隐私问题。

在接下来的篇幅里,我将阐述pico.js的理论背景及其工作原理。

Pico物件监测框架

2013年,Markus团队在一个技术报告中介绍了这一由Java实现的pico.js程式码库。它是参考C语言实现的,我们可在GitHub上获取其源代码:https://github.com/nenadmarkus/pico。我们密切关注其实现方法,因为我们不打算复制学习过程,而仅关注它的执行。这背后的原因是,我们最好学习带有官方程式码的检测器,将其载入到Java中并执行程式,如此就带有独特的优势(比如跨操作系统与装置的强大的可移植性)。

Pico物件检测框架是流行的Viola-Jones方法的一个改进。

Viola-Jones方法是基于区域分类的概念。这意味着在影象的每个合理位置和尺度上都使用分类器。这个区域列举过程的视觉化如下图所示:

该分类器试图判断当前区域是否存在人脸。最后,获取到的人脸区域将根据重叠程度进行聚类。鉴于每张影象都有很多区域,在这实时程式中有两个小技巧:

分类级联由一系列分类器组成。这些分类器中的每一个都能正确识别几乎所有的人脸,并丢弃一小部分非人脸区域。如果一个影象区域通过了级联的所有成员,那么它就被认定为人脸。通过(设计)序列中靠前的分类器比靠后的分类器更简单,这种效果得到了进一步放大。级联分类演算法如下图所示:

每个阶段包括一个分类器Cn,它既可以拒绝影象区域(R),也可以接受影象区域(A)。一旦被拒绝,该区域将不会进入下一级联成员。如果没有一个分类器拒绝该区域,我们认为它是一张人脸。

在Viola-Jones框架中,每个分类器Cn都基于Haar-like特性。这使得每个区域可通过名为积分影象的预算结构来进行O(1)计算时间。

然而,积分影象也有一些缺点。最明显的缺点是,这种资料结构需要额外的内存来储存:通常是unit8输入影象的4倍。另外一个问题是构建一个完整的影象所需的时间(也与输入的画素数有关)。在功能有限的小型硬件上处理大的影象也可能会有问题。这种方法的一个更微妙的问题是它的优雅性:随之而来的问题是我们是否能够建立一个不需要这种结构、并且具有所有重要属性的框架。

Pico框架对每个分类器Cn用画素对比测试取代了Haar-like特性,形式如下:

其中R是一个影象区域,(Xi,Yi)表示用于比较画素值的位置。注意,这种测试可以应用于各种尺寸的区域,而不需要任何专门的资料结构,这与Haar-like的特性不同。这是通过将位置(Xi,Yi)储存在标准化座标中(例如,(Xi,Yi)在[−1,1]×[−1,1]中),并乘以当前区域的比例。这就是pico实现多尺度检测功能的思路。

由于此类测试很简单,又因混叠和噪声而存在潜在问题,我们有必要将大量测试应用于该区域,以便对其内容进行推理。在pico框架中,这是通过

其中Tt(R)表示决策树Tt在输入区域R上生成的标量输出。由于每个决策树都由若干个画素比较测试组成,这些测试可以根据需要调整大小,因此执行分类阶段Cn的计算复杂度与区域大小无关。

每个Cn决策树都是AdaBoost的变体。接下来以这种方式将阈值设定为Cn的输出,以获取期望的真阳率(例如0.995)。所有得分低于这个阈值的区域都不认为是人脸。新增级联的新成员,直到达到预期的假阳率。请参阅原出版物学习相关细节内容。

正如简介中说的那样,我们不会复制pico的学习过程,而仅关注它的执行。如果您想学习自定义物件/人脸检测器,请使用官方的实现方法。Pico.js能够载入二进位制级联档案并有效地处理影象。接下来的小节将解释如何使用pico.js来检测影象中的人脸。

pico.js的元件

库的组成部分如下:

通过>(或它的压缩版本) 引入并进行一些预处理后,就可以使用这些工具了。我们将讨论对影象进行人脸检测的JS程式码(GitHub repo中的程式码)。但愿这能详尽说明使用该库的方法。实时演示也有说明。

区域分类器应识别影象区域是否为人脸。其思路是在整个影象中执行这个分类器,以获得其中的所有面孔(稍后详细介绍)。Pico.js的区域分类过程封装在一个函式中,其原型如下:

function(r, c, s, pixels, ldim) {

/*

...

*/

}

前三个引数(r、c和s)指定区域的位置(其中心的行和列)及其大小。pixels阵列包含影象的灰度强度值。引数ldim规定从影象的一行移动到下一行的方式(在诸如OpenCV的库中称为stride)。也就是说,从程式码中可以看出(r,c)位置的画素强度为[r*ldim + c]画素。该函式会返回一个浮点值,表示该区域的得分。如果分数大于或等于0.0,则该区域认定为人脸。如果分数低于0.0,则该区域认定为非人脸,即属于背景类。

Pico.js中pico.unpack_cascade过程将二进位制的级联作为引数,将其解压并返回一个带有分类过程和分类器资料的闭包函式。我们用它初始化区域分类过程,以下是详细说明。

官方pico的人脸检测级联称为facefinder。它由近450个决策树组成,每个决策树的深度为6,它们整合一个25级联。该级联将在我们是实验中用到,它能对正脸影象以适当的检测速率进行实时处理,正如实时演示看到的那样。

facefinder级联可以直接从官方的github库上下载,程式码写为:

varfacefinder_classify_region = function(r, c, s, pixels, ldim) { return-1.0;};

varcascadeurl = \https://raw.githubusercontent.com/nenadmarkus/pico/c2e81f9d23cc11d1a612fd21e4f9de0921a5d0d9/rnt/cascades/facefinder\;

fetch(cascadeurl).then( function(response) {

response.arrayBuffer.then( function(buffer) {

varbytes = newInt8Array(buffer);

facefinder_classify_region = pico.unpack_cascade(bytes);

console.log( \* cascade loaded\);

})

})

首先,将facefinder_classify_region初始化,即任何影象区域先认定为非人脸(它总是返回-1.0)。接下来,我们使用Fetch API从cascadeurl URL中获取级联二进位制资料。这是一个异步呼叫,我们不能即刻获取到资料。最后,在获取到响应资料后,将其转换为int8阵列并传递给pico.unpack_cascade,然后pico.unpack_cascade生成正确的facefinder_classify_region函式。

将facefinder_classify_region函式应用于影象中每个区域的合理位置和等级以便检测到所有的人脸。这个过程将在下一小节中解释。

假定HTML body内有一个canvas元素,一个image标签和一个带有onclick回拨的button标签。使用者一旦点选了人脸检测按钮,检测过程就开始了。

下面的JS程式码用于绘制内容和影象,并获取原始画素值(红、绿、蓝+ alpha的格式):

varimg = document.getElementById( \image\);

varctx = document.getElementById( \canvas\).getContext( \2d\);

ctx.drawImage(img, 0, 0);

varrgba = ctx.getImageData( 0, 0, 480, 360).data; // the size of the image is 480x360 (width x height)

下面,我们编写一个辅助函式,将输入的RGBA阵列转换为灰度:

functionrgba_to_grayscale(rgba, nrows, ncols) {

vargray = newUint8Array(nrows*ncols);