带肤色和运动分析的 C++ 人脸检测库






4.92/5 (236投票s)
本文介绍了一个经过 SSE 优化的 C++ 人脸检测库,可处理彩色和灰度数据,并具有肤色检测、用于加速处理的运动估算、小尺寸 SVM 和 NN 粗略人脸预过滤、PCA/LDA/ICA/任何降维/投影以及最终的 NN 分类。

目录
引言
对于不受控场景(户外环境、机场、火车站/公交车站)中的人脸识别,进行智能图像/视频处理的第一步是人脸检测。后者精度很大程度上取决于你的人脸识别结果。通常,该领域的研究人员没有时间或能力开发可商用的优化 C++ 代码,而仅限于 Matlab 开发过程。
本文的目的是提供一个我开发的 SSE 优化 C++ 人脸检测库,以便你现在就可以在视频监控应用中使用它。随库提供的分类器是在我一段时间内收集的、在不同光照条件下使用网络摄像头拍摄的图像上训练的,并且它能实时、无误地检测出我。对于非人脸数据,我使用了一些网络摄像头拍摄的背景快照。从图像中提取的矩形区域会经过高斯滤波,然后进行直方图归一化,这样,如果存在人脸,经过该过程后,它们的外观会相当相似。你可以在下面找到其背后的算法描述,如果你在检测你的特定身份时遇到问题,可以尝试使用互联网上大量可用的数据库中的更多不同人脸来重新训练网络,就像我的库使用的 CBCL 一样,大小为 19x19。它超过 100MB,而我目前没有宽带接入。
背景
建议了解小波分析、降维方法(PCA、LDA、ICA)、人工神经网络(ANN)、支持向量机(SVM)、SSE 编程、图像处理、形态学操作(腐蚀、膨胀)、直方图均衡化、图像滤波、C++ 编程以及一些人脸检测背景知识。我不会详细介绍提到的每一点,因为那样会写很多,你应该查阅 Google 或 Wiki,那里有大量的教程。
Using the Code
程序的 MFC 界面取自我的另一个项目 Video Preview and Frames Capture to Memory with SampleGrabber in Buffered Mode。虽然编译的演示版本被初始化为 640x480 的视频流,但如果你的网络摄像头只支持不同的帧率,你需要在 CVidCapDlg
构造函数中重新编译并初始化 m_Width
和 m_Height
变量为你自己的分辨率。如果你的网络摄像头运行在 320x240,这比 640x480 小一半,你也需要将 m_ResizeRatio
从 0.125 更改为 0.25(翻倍)。之所以进行这样的修改,是因为为了实时运行,在原始 640x480 图像上搜索 19x19 大小的人脸(非常非常小)成本很高。我使用我文章 Fast Dyadic Image Scaling with Haar Transform 中的代码将图像缩小到原始尺寸的 0.125:80x60。现在,如果你开始在缩小后的图像上搜索 19x19 的矩形人脸,它在 640x480 图像上的实际尺寸将是原来的 8 倍:152x152。降采样不仅提高了计算速度,还获得了没有噪声和多余细节的粗略人脸版本。在这种降采样下,所有的人脸看起来都非常相似,这有利于检测。在 640x480 图像上,152x152 的人脸大小就像你坐在显示器前大约的距离。所以,如果你希望计算机能看到你,你应该坐在那个距离或更近。对于你的 320x240 网络摄像头视频流,你需要将图像缩小到原始尺寸的 0.25。缩小到 80x60 的图像会进一步插值为原始尺寸的 0.86、0.73 和 0.6。因此,构建了图像金字塔,并在该图像金字塔上实现人脸搜索,这使得计算机能够“看到”人脸的大小,你离网络摄像头越近,识别出的人脸矩形就越大。
要开始检测过程,请单击枚举按钮选择视频源设备,选择捕获速率(以毫秒为单位),默认值为 1000(每秒捕获一帧),勾选检测框以加载或卸载分类器,你将看到 cvInfo 被替换为
classifiers: 15
image: 80x60
scales: 0.86, 0.73, 0.60
已加载的分类器位于可执行文件同目录下的 face.nn、pca.nn、preflt.nn 和 skin.nn 文件中。如果光照条件不佳,你可以通过简单删除 skin.nn 文件来跳过肤色检测过程,在这种情况下,分类器的状态将是
classifiers: 11
image: 80x60
scales: 0.86, 0.73, 0.60
现在单击“运行”按钮,然后观察你的脸在中间较大的静态控件中被检测到。在我的 2.2Ghz AMD MK-38 上,它在大约 15-25fps 的速度下检测到人脸,但有时甚至能达到 35-50fps - 请查看结果部分。因此,我可以设置帧捕获速率为 100(每秒 10 帧),而不会对处理器造成太大负担。
算法
该算法并非新颖,已在许多科学论文中报道过。缩小后的图像用高斯滤波器平滑以减少噪声,然后搜索 19x19 的矩形,这些矩形在分类过程之前经过直方图均衡化。为了进一步降低由 19x19 矩阵列拼接组成的 361 维列向量的维度,我们可以将其线性投影到正交 PCA、LDA 或非正交 ICA 投影矩阵上,这些矩阵的列是相同维度的投影基向量。在我的投影矩阵中,我首先使用了从我的训练人脸/非人脸集合 PCA 变换获得的 40 个特征向量。接下来的 2 张图像显示了我训练集中使用的一些人脸和非人脸。


总共,我使用了大约 1800 个带有人脸的向量和 34000 个非人脸用于 PCA、LDA 和 ICA 变换。因此,用于获得投影基的变换矩阵大小为 35800x361。投影到 361x40 PCA、LDA 或 ICA 投影矩阵后的 40 维训练样本,被进一步用于训练一个由 4 层(分别包含 40、20、10 和 1 个神经元)组成的人工神经网络分类器。PCA 投影产生了最佳的分类结果,这决定了降维方法的选择。
我算法中使用的 40 个 PCA 特征向量如下所示

它们包含约 95% 的方差。投影数据如下所示,表示为均值 ± 标准差。红色表示人脸数据,绿色表示非人脸数据。我们可以看到,在前 25 个投影中,均值之间的变异性更大,其余的可能归因于噪声。我希望在不久的将来对不同数量的投影分量运行 NN 训练和分类会话,并发布结果。

下面给出了第 4 个和第 9 个投影向量的散点图,以了解我们的 PCA 投影数据在 2D 空间中的分布情况。对于各种投影分量组合的其余 2D 散点图具有相同的外观。可惜我们不能以同样的方式绘制 40 维空间。

LDA 算法仅能很好地分离第一个分量,其余的分量均值相同。LDA 投影数据如下所示

下图显示了第 1 个和第 2 个投影 LDA 分量的散点图,红色表示人脸向量,绿色表示非人脸。

我喜欢从人脸样本(1800x361)组成的矩阵中获得的特征向量(特征脸)的外观,因为它们与训练矩阵中存在的人脸相似。

一些方法仅使用上面显示的特征脸作为投影基。
对于 ANN 训练过程,我将 1800 张人脸和 34000 个非人脸随机分成两半,用于训练集,另一半用于验证集和测试集。验证集用作停止标准,测试集是网络泛化能力的独立估计。我使用了 ANN 反向传播梯度下降训练算法,并带有关联 Backpropagation Artificial Neural Network in C++ 以及 Matlab 神经网络工具箱,该工具箱使用反向传播梯度下降算法,并带有关联和自适应学习率。我的 ANN 实现大约需要 10 秒在 2.2Ghz 上训练网络。训练会话的最佳结果如下所示,包括训练集、验证集和测试集的灵敏度、特异性、阳性和阴性预测值以及准确度。
ann1Dn.exe t pca19x19.nn scrpca void 100 void void 0.5 3
TRAINING SET: 36097
VALIDATION SET: 9023
TEST SET: 9475
loading data...
cls1: 1787 cls2: 34310 files loaded. size: 40 samples
validation size: 446 8577
test size: 469 9006
training...
epoch: 1 out: 0.876908 0.109924 max acur: 0.88 (epoch 1)
se:98.65 sp:98.66 ac:98.66
epoch: 2 out: 0.908594 0.084828 max acur: 0.90 (epoch 2)
se:99.55 sp:98.78 ac:98.81
epoch: 3 out: 0.915247 0.083308 max acur: 0.93 (epoch 3)
se:98.43 sp:99.28 ac:99.24
epoch: 4 out: 0.919266 0.081627 max acur: 0.94 (epoch 4)
se:98.43 sp:99.45 ac:99.40
epoch: 5 out: 0.922610 0.083322 max acur: 0.95 (epoch 5)
se:97.98 sp:99.52 ac:99.45
epoch: 6 out: 0.925658 0.084236 max acur: 0.96 (epoch 6)
se:98.21 sp:99.66 ac:99.59
epoch: 7 out: 0.927656 0.084170 max acur: 0.96 (epoch 6)
se:98.21 sp:99.66 ac:99.59
epoch: 8 out: 0.928191 0.085515 max acur: 0.97 (epoch 8)
se:98.88 sp:99.77 ac:99.72
epoch: 9 out: 0.927756 0.088423 max acur: 0.97 (epoch 8)
se:98.88 sp:99.77 ac:99.72
epoch: 10 out: 0.929448 0.087034 max acur: 0.97 (epoch 8)
se:98.88 sp:99.77 ac:99.72
training done.
classification results: maxacur.nn
train set: 872 16727
sensitivity: 100.00
specificity: 99.96
+predictive: 99.20
-predictive: 100.00
accuracy: 99.96
validation set: 446 8577
sensitivity: 98.88
specificity: 99.77
+predictive: 95.66
-predictive: 99.94
accuracy: 99.72
test set: 469 9006
sensitivity: 98.93
specificity: 99.77
+predictive: 95.67
-predictive: 99.94
accuracy: 99.73
然而,Matlab 提供了稍好的性能,但训练会话持续几分钟。
net = train2(net,{face;nface},1000,'traingdx');
TRAINGDX-calcgrad, Epoch 0/1000, MSE 0.0380841/0,
Gradient 0.0161943/1e-006
TRAINGDX-calcgrad, Epoch 25/1000, MSE 0.0379897/0,
Gradient 0.0160989/1e-006
TRAINGDX-calcgrad, Epoch 50/1000, MSE 0.0377002/0,
Gradient 0.0157962/1e-006
TRAINGDX-calcgrad, Epoch 75/1000, MSE 0.0367918/0,
Gradient 0.0148357/1e-006
TRAINGDX-calcgrad, Epoch 100/1000, MSE 0.0342653/0,
Gradient 0.0131359/1e-006
TRAINGDX-calcgrad, Epoch 125/1000, MSE 0.0288224/0,
Gradient 0.00899192/1e-006
TRAINGDX-calcgrad, Epoch 150/1000, MSE 0.0195839/0,
Gradient 0.00661213/1e-006
TRAINGDX-calcgrad, Epoch 175/1000, MSE 0.0114634/0,
Gradient 0.00248487/1e-006
TRAINGDX-calcgrad, Epoch 200/1000, MSE 0.00730671/0,
Gradient 0.00141872/1e-006
TRAINGDX-calcgrad, Epoch 225/1000, MSE 0.00619382/0,
Gradient 0.0139478/1e-006
TRAINGDX-calcgrad, Epoch 250/1000, MSE 0.00552043/0,
Gradient 0.00225788/1e-006
TRAINGDX-calcgrad, Epoch 275/1000, MSE 0.00506328/0,
Gradient 0.00117316/1e-006
TRAINGDX-calcgrad, Epoch 300/1000, MSE 0.00394131/0,
Gradient 0.000806827/1e-006
TRAINGDX-calcgrad, Epoch 325/1000, MSE 0.00304252/0,
Gradient 0.00243748/1e-006
TRAINGDX-calcgrad, Epoch 350/1000, MSE 0.00297078/0,
Gradient 0.00102562/1e-006
TRAINGDX-calcgrad, Epoch 375/1000, MSE 0.0028294/0,
Gradient 0.00061697/1e-006
TRAINGDX-calcgrad, Epoch 400/1000, MSE 0.00243267/0,
Gradient 0.000526016/1e-006
TRAINGDX-calcgrad, Epoch 425/1000, MSE 0.00187066/0,
Gradient 0.00242446/1e-006
TRAINGDX-calcgrad, Epoch 450/1000, MSE 0.00183145/0,
Gradient 0.000864909/1e-006
TRAINGDX-calcgrad, Epoch 475/1000, MSE 0.0017861/0,
Gradient 0.000520862/1e-006
TRAINGDX-calcgrad, Epoch 500/1000, MSE 0.00167154/0,
Gradient 0.000361654/1e-006
TRAINGDX-calcgrad, Epoch 525/1000, MSE 0.00136531/0,
Gradient 0.000335621/1e-006
TRAINGDX-calcgrad, Epoch 550/1000, MSE 0.00132639/0,
Gradient 0.00278421/1e-006
TRAINGDX-calcgrad, Epoch 575/1000, MSE 0.00128443/0,
Gradient 0.000823477/1e-006
TRAINGDX-calcgrad, Epoch 600/1000, MSE 0.00123788/0,
Gradient 0.000294566/1e-006
TRAINGDX-calcgrad, Epoch 625/1000, MSE 0.00110702/0,
Gradient 0.000240032/1e-006
TRAINGDX-calcgrad, Epoch 650/1000, MSE 0.000992816/0,
Gradient 0.00162968/1e-006
TRAINGDX-calcgrad, Epoch 675/1000, MSE 0.000973248/0,
Gradient 0.000397771/1e-006
TRAINGDX-calcgrad, Epoch 700/1000, MSE 0.000951908/0,
Gradient 0.000277682/1e-006
TRAINGDX-calcgrad, Epoch 725/1000, MSE 0.000891638/0,
Gradient 0.000194585/1e-006
TRAINGDX-calcgrad, Epoch 750/1000, MSE 0.000745773/0,
Gradient 0.000971981/1e-006
TRAINGDX-calcgrad, Epoch 775/1000, MSE 0.000738705/0,
Gradient 0.000281221/1e-006
TRAINGDX-calcgrad, Epoch 800/1000, MSE 0.000731111/0,
Gradient 0.000223325/1e-006
TRAINGDX-calcgrad, Epoch 825/1000, MSE 0.000708551/0,
Gradient 0.0001534/1e-006
TRAINGDX-calcgrad, Epoch 850/1000, MSE 0.000642279/0,
Gradient 0.000133356/1e-006
TRAINGDX-calcgrad, Epoch 875/1000, MSE 0.000624203/0,
Gradient 0.00193949/1e-006
TRAINGDX-calcgrad, Epoch 900/1000, MSE 0.000604328/0,
Gradient 0.000337345/1e-006
TRAINGDX-calcgrad, Epoch 925/1000, MSE 0.000593177/0,
Gradient 0.000146603/1e-006
TRAINGDX-calcgrad, Epoch 950/1000, MSE 0.000560221/0,
Gradient 0.000115208/1e-006
TRAINGDX-calcgrad, Epoch 975/1000, MSE 0.000535758/0,
Gradient 0.00183329/1e-006
TRAINGDX-calcgrad, Epoch 1000/1000, MSE 0.000518507/0,
Gradient 0.000550508/1e-006
TRAINGDX, Maximum epoch reached, performance goal was not met.
train set: 893 17154
sensitivity: 99.55
specificity: 100.00
+predictive: 100.00
-predictive: 99.98
accuracy: 99.98
mse: 0.000519
validation set: 447 8578
sensitivity: 96.42
specificity: 99.90
+predictive: 97.95
-predictive: 99.81
accuracy: 99.72
mse: 0.001824
test set: 447 8578
sensitivity: 97.32
specificity: 99.90
+predictive: 97.97
-predictive: 99.86
accuracy: 99.77
mse: 0.001746
与收敛速度快但对验证集和测试集泛化性能不佳的其他训练算法相比,Matlab 的 traingdx
方法提供了最佳分类结果。
我在 ANN 分类过程中,使用了不同数量的 PCA 分量,获得了测试集灵敏度和阳性预测值几何平均值方面的分类准确率。如前所述,在第 25 个分量之后,分类率变化不大。

接下来,描述了一些预处理技巧,这些技巧有助于显著减小搜索区域。
运动检测
你可以通过利用视频流中连续帧之间的时间信息来显著减小人脸的搜索区域。我实现了一个简单的运动检测器类,该类会保留连续帧之间的差异。因此,你只需要搜索检测到运动的区域。为了不丢失已检测到的人脸,如果人保持静止,我会标记该人脸矩形区域内的像素,并在下一帧中,人脸检测器被触发在该区域再次搜索。
肤色检测
我使用 RGB 数据中的颜色信息来检测肤色区域,并仅在这些区域内进行搜索。如果不是黑暗且光照充足,你可能不需要在蓝色或绿色区域查找人脸。我只使用了白种人的肤色样本,因为我没有其他国籍人士的照片,但你可以收集这些照片,并为不同肤色类型安排不同的肤色检测器,并互换使用。对于非肤色数据,我使用了一些背景彩色图片。颜色数据是一个三维向量,包含 0 到 255 范围内的红、绿、蓝三个通道的值,在分类之前我将其缩放到 0.0...1.0 的范围。我为此目的使用了 ANN 分类器,该分类器由 3、6 和 1 个神经元组成的 3 层构成。再次,Matlab 会话提供了比我的 ANN 代码稍好的性能。该网络非常简洁,所以我在此展示完整的文件。稍后我将介绍文件格式。
3
3 6 1
0
1
0.000000 1.000000
0.000000 1.000000
0.000000 1.000000
12.715479
3.975381
7.267306
-2.828213
-5.901746
56.047752
-31.722653
-21.872528
-10.393643
-11.324373
42.869545
-27.680447
3.026848
-8.795427
-32.185852
62.505981
8.211640
-32.108111
81.444914
-48.067621
8.276753
19.659469
-50.115191
27.516311
-15.402503
-9.760841
4.400588
18.516097
0.624907
4.414670
17.972277
net = train2(net,{skin;nskin},1000);
TRAINLM-calcjx, Epoch 0/1000, MSE 0.0775247/0, Gradient 0.0431253/1e-010
TRAINLM-calcjx, Epoch 25/1000, MSE 0.00730059/0, Gradient 0.000136228/1e-010
TRAINLM-calcjx, Epoch 50/1000, MSE 0.00658964/0, Gradient 6.8167e-005/1e-010
TRAINLM-calcjx, Epoch 64/1000, MSE 0.00657199/0, Gradient 1.68806e-005/1e-010
TRAINLM, Validation stop.
train set: 2959 6383
se: 98.11
sp: 99.09
pp: 98.04
np: 99.12
ac: 98.78
er: 0.006637
validation set: 1479 3191
se: 98.78
sp: 99.22
pp: 98.32
np: 99.43
ac: 99.08
er: 0.005474
test set: 1479 3191
se: 97.77
sp: 99.22
pp: 98.30
np: 98.97
ac: 98.76
er: 0.006467
如果你同时使用运动和肤色检测,则从检测中获得的掩码会通过 AND
操作合并。
粗略非人脸排除
在计算从 361 维空间到 40 维空间的昂贵投影(即 40 次对 361 维向量的卷积运算(40 * (361 次乘法 + 360 次加法)))之前,你可以构建小型分类器,具有少量隐藏神经元的 ANN 或仅具有两个支持向量的 SVM。它们单独很难产生高检测率,但具有高特异性率(正确非人脸检测的百分比)可以拒绝绝大多数非人脸区域。在检测为阳性的情况下,我将继续进行 PCA 投影和进一步的分类阶段。
我设计了 ANN 分类器,由 361、3 和 1 个神经元组成的 3 层构成。使用这种 ANN 结构,我们只需对 361 维向量进行 3 次卷积(3 * (361 次乘法 + 360 次加法)),在拒绝非人脸矩形的情况下,性能大约提高 13.3 倍。为了训练,我使用了整个数据集,因为隐藏神经元的数量非常非常少,我们不必担心过拟合。下面显示了 Matlab 的训练会话。
net = train2(net, {face;nface}, 1000, 'trainscg');
TRAINSCG-calcgrad, Epoch 0/1000, MSE 0.0706625/0,
Gradient 0.0408157/1e-006
TRAINSCG-calcgrad, Epoch 25/1000, MSE 0.0252248/0,
Gradient 0.00524303/1e-006
TRAINSCG-calcgrad, Epoch 50/1000, MSE 0.0208643/0,
Gradient 0.00410831/1e-006
TRAINSCG-calcgrad, Epoch 75/1000, MSE 0.0199327/0,
Gradient 0.00245891/1e-006
TRAINSCG-calcgrad, Epoch 100/1000, MSE 0.0183846/0,
Gradient 0.00243653/1e-006
TRAINSCG-calcgrad, Epoch 125/1000, MSE 0.0167635/0,
Gradient 0.00413765/1e-006
TRAINSCG-calcgrad, Epoch 150/1000, MSE 0.0156056/0,
Gradient 0.00222181/1e-006
TRAINSCG-calcgrad, Epoch 175/1000, MSE 0.0145851/0,
Gradient 0.00154122/1e-006
TRAINSCG-calcgrad, Epoch 200/1000, MSE 0.0138646/0,
Gradient 0.00187787/1e-006
TRAINSCG-calcgrad, Epoch 225/1000, MSE 0.0130431/0,
Gradient 0.00110514/1e-006
TRAINSCG-calcgrad, Epoch 250/1000, MSE 0.0124017/0,
Gradient 0.000794686/1e-006
TRAINSCG-calcgrad, Epoch 275/1000, MSE 0.0119851/0,
Gradient 0.000686196/1e-006
TRAINSCG-calcgrad, Epoch 300/1000, MSE 0.0115745/0,
Gradient 0.000538587/1e-006
TRAINSCG-calcgrad, Epoch 325/1000, MSE 0.0110115/0,
Gradient 0.000619825/1e-006
TRAINSCG-calcgrad, Epoch 350/1000, MSE 0.0106856/0,
Gradient 0.000397777/1e-006
TRAINSCG-calcgrad, Epoch 375/1000, MSE 0.0103944/0,
Gradient 0.000335075/1e-006
TRAINSCG-calcgrad, Epoch 400/1000, MSE 0.0101617/0,
Gradient 0.000491891/1e-006
TRAINSCG-calcgrad, Epoch 425/1000, MSE 0.00997477/0,
Gradient 0.00054992/1e-006
TRAINSCG-calcgrad, Epoch 450/1000, MSE 0.00982742/0,
Gradient 0.000277635/1e-006
TRAINSCG-calcgrad, Epoch 475/1000, MSE 0.00973586/0,
Gradient 0.000246542/1e-006
TRAINSCG-calcgrad, Epoch 500/1000, MSE 0.00964214/0,
Gradient 0.000430392/1e-006
TRAINSCG-calcgrad, Epoch 525/1000, MSE 0.00955025/0,
Gradient 0.000208358/1e-006
TRAINSCG-calcgrad, Epoch 550/1000, MSE 0.00946008/0,
Gradient 0.000259309/1e-006
TRAINSCG-calcgrad, Epoch 575/1000, MSE 0.00935917/0,
Gradient 0.000293115/1e-006
TRAINSCG-calcgrad, Epoch 600/1000, MSE 0.00928761/0,
Gradient 0.000297855/1e-006
TRAINSCG-calcgrad, Epoch 625/1000, MSE 0.00922528/0,
Gradient 0.000220927/1e-006
TRAINSCG-calcgrad, Epoch 650/1000, MSE 0.0091606/0,
Gradient 0.000237646/1e-006
TRAINSCG-calcgrad, Epoch 675/1000, MSE 0.0091048/0,
Gradient 0.000301604/1e-006
TRAINSCG-calcgrad, Epoch 700/1000, MSE 0.00903629/0,
Gradient 0.000338506/1e-006
TRAINSCG-calcgrad, Epoch 725/1000, MSE 0.00897432/0,
Gradient 0.000207054/1e-006
TRAINSCG-calcgrad, Epoch 750/1000, MSE 0.00892802/0,
Gradient 0.000189826/1e-006
TRAINSCG-calcgrad, Epoch 775/1000, MSE 0.00888097/0,
Gradient 0.000197304/1e-006
TRAINSCG-calcgrad, Epoch 800/1000, MSE 0.00882134/0,
Gradient 0.00015089/1e-006
TRAINSCG-calcgrad, Epoch 825/1000, MSE 0.00875718/0,
Gradient 0.000262308/1e-006
TRAINSCG-calcgrad, Epoch 850/1000, MSE 0.00870468/0,
Gradient 0.000160878/1e-006
TRAINSCG-calcgrad, Epoch 875/1000, MSE 0.00865585/0,
Gradient 0.000154301/1e-006
TRAINSCG-calcgrad, Epoch 900/1000, MSE 0.00860733/0,
Gradient 0.000194127/1e-006
TRAINSCG-calcgrad, Epoch 925/1000, MSE 0.00853522/0,
Gradient 0.000360248/1e-006
TRAINSCG-calcgrad, Epoch 950/1000, MSE 0.00847012/0,
Gradient 0.000149492/1e-006
TRAINSCG-calcgrad, Epoch 975/1000, MSE 0.00841884/0,
Gradient 0.00017306/1e-006
TRAINSCG-calcgrad, Epoch 1000/1000, MSE 0.0083688/0,
Gradient 0.000173782/1e-006
TRAINSCG, Maximum epoch reached, performance goal was not met.
elapsed time: 18.441611 min
train set: 1788 17853
se: 95.08
sp: 99.16
pp: 91.89
np: 99.51
ac: 98.79
er: 0.008369
我们可以看到,非人脸向量的正确检测百分比为 99.16%,这是一个极其稳健的比率。
下一张图片显示了最后一个阶段 ANN 分类器的输出,该分类器接收 PCA 转换后的 19x19 数据向量。分类输出范围为 0.0-1.0,因为 ANN 的输出层使用了 sigmoid 函数。

上图显示了 PCA 转换向量的 ANN 检测。由于神经网络被训练为将非人脸的输出匹配为 0.1,将人脸匹配为 0.9,因此整体图像为浅蓝色,中间有一个匹配找到的人脸的热红色斑点。周围的纯蓝色边框(设置为零)是我搜索整个 19x19 矩形适合的区域。下图显示了预过滤器和 PCA 转换 ANN 分类的组合。如果预过滤器将矩形拒绝为非人脸类别,我输出 0.0,否则继续进行 PCA 转换和 ANN 分类。你可以看到,上半图中所有的浅蓝色区域都被零(纯蓝色)取代了。
除了 ANN 预过滤器,我还设计了一个线性 SVM 分类器,该分类器仅包含 2 个支持向量,代表训练集中的人脸和非人脸数据的均值。它的处理速度比 ANN 更快,只需对 361 维向量进行两次卷积,但性能稍差,非人脸向量的正确检测率为 76.72%。训练集上的分类如下所示。
sensitivity: 93.85
specificity: 76.72
+predictive: 28.76
-predictive: 99.20
accuracy: 78.28
你可以使用其中任何一个,只需将相应的分类器重命名为文件 preflt.nn。
库说明
人脸检测库中的主要类,你需要使用它们来构建你自己的应用程序是:
ImageResize
MotionDetector
FaceDetector
我将它们封装在 cvlib.cpp 文件中,以便在 MFC 应用程序中更轻松地使用。ImageResize
在我的文章 Fast Dyadic Image Scaling with Haar Transform 中有描述。MotionDetector
类非常简单。你所要做的就是将其初始化为降采样图像的宽度和高度。你可能还记得,我在 MFC 应用程序中使用 640x480 的视频流,并将其降采样到原始尺寸的 0.125:80x60。然后,只需使用其 MotionDetector::detect()
方法,提供从 ImageResize
降采样后的图像。
-
void MotionDetector::init(unsigned int image_width, unsigned int image_height);
-
const vec2Dc* MotionDetector::detect(const vec2D* frame, const FaceDetector* fdetect);
你需要将你使用的 FaceDetector
对象作为第二个参数提供给 MotionDetector::detect()
。因为它会在其帧中标记找到的人脸矩形作为搜索区域,所以当你使用的运动停止时,你不会丢失已检测到的人脸,如我上面所述。vec2D
是 SSE 优化的浮点 2D 向量,你可以在我的文章 Vector Class Wrapper SSE Optimized for Math Operations 中找到。它的“小兄弟”vec2Dc
是一个 char
类型对象,具有与 vec2D
类似的接口。在 MotionDetector::detect()
之后,你将收到一个指向 vec2Dc
对象的指针,其中零条目表示没有运动,非零条目表示运动区域。
这是 MotionDetector::detect()
函数的代码。
const vec2Dc* MotionDetector::detect
(const vec2D* frame, const FaceDetector* fdetect)
{
if (status() < 0)
return 0;
//m_last = frame - m_last
m_last_frame->sub(*frame, *m_last_frame);
//set to 1.0 faces rects
RECT r0;
for (unsigned int i = 0; i < fdetect->get_faces_number(); i++) {
const RECT* r = fdetect->get_face_rect(i);
r0.left = r->left;
r0.top = r->top;
r0.right = r->right;
r0.bottom = r->bottom;
m_last_frame->set(255.0f, r0);
}
for (unsigned int y = fdetect->get_dy();
y < m_motion_vector->height() - fdetect->get_dy(); y++) {
for (unsigned int x = fdetect->get_dx();
x < m_motion_vector->width() - fdetect->get_dx(); x++) {
if (fabs((*m_last_frame)(y, x)) > m_TH)
(*m_motion_vector)(y, x) = 1;
else
(*m_motion_vector)(y, x) = 0;
}
}
*m_last_frame = *frame;
return m_motion_vector;
}
之后,你就可以继续进行实际的人脸检测过程了。在使用 FaceDetector
对象之前,你也需要初始化它。我通过以下方式在 cvlib.cpp 包装器中实现了初始化和检测:
float m_zoom = 0.125f;
vector<float> m_scales;
ImageResize resize;
MotionDetector mdetect;
FaceDetector fdetect;
void cvInit(unsigned int image_width, unsigned int image_height,
unsigned int face_width, unsigned int face_height, double zoom)
{
m_zoom = (float)zoom;
resize.init(image_width, image_height, m_zoom);
mdetect.init(resize.gety()->width(), resize.gety()->height());
if (m_scales.size()) {
fdetect.init(resize.gety()->width(), resize.gety()->height(),
face_width, face_height,
&m_scales[0], (unsigned int)m_scales.size());
} else
fdetect.init(resize.gety()->width(), resize.gety()->height(),
face_width, face_height);
}
int cvInitAI(const wchar_t* face_detector,
const wchar_t* projection_matrix,
const wchar_t* skin_filter,
const wchar_t* preface_filter) //0-OK, <0-err
{
int res;
res = fdetect.load_face_classifier(face_detector);
if (res != 0)
return res;
res = fdetect.load_projection_matrix(projection_matrix);
if (res != 0)
return res;
if (skin_filter != 0)
fdetect.load_skin_filter(skin_filter) ;
if (preface_filter != 0)
fdetect.load_preface_filter(preface_filter);
return fdetect.status_of_classifiers();
}
void cvSetScales(const double* scales, unsigned int size)
{
m_scales.clear();
for (unsigned int i = 0; i < size; i++)
m_scales.push_back((float)scales[i]);
}
cvInit()
函数中的 image_width
和 image_height
是视频流中图像的原始尺寸,在我的情况下是 640x480。face_width
和 face_height
是 19x19 的人脸矩形尺寸。使用 cvSetScales()
,我添加了 3 个额外的尺度,将 80x60 图像插值到这些尺度:0.86、0.73 和 0.6。因此,计算机将能够“看到”你的人脸大小。接下来,你继续初始化 FaceDetector
对象。
-
void FaceDetector::init(unsigned int image_width, unsigned int image_height, unsigned int face_width, unsigned int face_height, const float* scales = 0, unsigned int nscales = 0);
这里的 image_width
和 image_height
是你图像的降采样尺寸。scales
指向包含 0.86、0.73、0.6 数字的数组,就我而言,但你可以提供你自己的选择,例如 1.3、0.8、0.7、0.5,计算机将看到你随着远离网络摄像头而以 80x60 图像的 1.3 倍放大版本显示。
使用以下函数,你只需加载和卸载分类器。
-
int FaceDetector::load_skin_filter(const wchar_t* fname);
-
void FaceDetector::unload_skin_filter();
-
int FaceDetector::load_preface_filter(const wchar_t* fname);
-
void FaceDetector::unload_preface_filter();
-
int FaceDetector::load_projection_matrix(const wchar_t* fname);
-
void FaceDetector::unload_projection_matrix();
-
int FaceDetector::load_face_classifier(const wchar_t* fname);
-
void FaceDetector::unload_face_classifier();
它们的名称不言自明,成功时返回 0。
在我的库中,你可以找到 ANNetwork
和 SVMachine
分类器。它们使用 vec2D
类进行 SSE 优化的矩阵运算。我将在未来发布一篇关于该主题的文章,并描述我用于训练的神经网络分类器。否则,如果你熟悉 ANN 和 SVM 算法,你可以轻松理解代码。我已经将它们封装在 AIClassifier
类中,因此你可以用一个函数加载 ANN 或 SVM 文件。但是,建议将 AIClassifier
实现为 ABC
类,并将 ANNetwork
、SVMachine
作为实现。希望将来我能做出这样的改变。
人脸检测过程如下:
int cvDetect(const unsigned char *pBGR)
{
wchar_t err[256] = L"";
if (resize.gety() == 0)
return -1;
__try {
resize.resize(pBGR);
return fdetect.detect(resize.gety(), resize.getr(),
resize.getg(), resize.getb(),
mdetect.detect(resize.gety(), &fdetect));
}
__except(EXCEPTION_EXECUTE_HANDLER) {
swprintf(err, 256, L"SEH error code: 0x%X",
GetExceptionCode());
MessageBox(0, L"error.", err, MB_OK|MB_ICONERROR);
return 0;
}
}
如果你的分辨率修改不当,我添加了 SEH 处理。然而,我还没有遇到任何 SEH 错误,并保证代码是健壮的。
你收到一个 RGB 数据流,其中第一个字节是三联体中的蓝色通道。接下来,我们使用 ImageResize::resize()
将其缩小 8 倍到 80x60,然后继续进行人脸检测。
-
int FaceDetector::detect(const vec2D* y, char** r, char** g, char** b, const vec2Dc* search_mask = 0);
它接收来自 ImageResize
类的灰度图像及其 R、G、B 通道。search_mask
是 MotionDetector::detect()
方法返回的 vec2Dc
向量,它是可选的。detect
函数在成功时返回 0 个或多个找到的人脸,如果初始化过程丢失或忘记加载分类器,则返回负值。
要访问检测到的人脸矩形,你可以使用这些函数:
-
inline unsigned int FaceDetector::get_faces_number() const;
-
inline const RECT* FaceDetector::get_face_rect(unsigned int i) const;
-
inline const vec2D* FaceDetector::get_face(unsigned int i) const;
我在 cvlib.cpp 包装器中提供了它们的实现。
unsigned int cvGetFacesNumber()
{
return fdetect.get_faces_number();
}
void cvFaceRect(unsigned int i, RECT& rect)
{
if (i < 0) i = 0;
if (i >= fdetect.get_faces_number()) i =
fdetect.get_faces_number() - 1;
const RECT* r = fdetect.get_face_rect(i);
rect.left = int((float)r->left * (1.0f / m_zoom));
rect.top = int((float)r->top * (1.0f / m_zoom));
rect.right = int((float)(r->right + 1) *
(1.0f / m_zoom) - (float)r->left * (1.0f / m_zoom));
rect.bottom = int((float)(r->bottom + 1) *
(1.0f / m_zoom) - (float)r->top * (1.0f / m_zoom));
}
const vec2D* cvGetFace(unsigned int i)
{
return fdetect.get_face(i);
}
不要忘记,RECT
结构体填充的是相对于降采样图像的人脸坐标,你必须将它们乘以你在 ImageResize
类中降采样图像的倍数,0.125 乘以 8 倍,0.25 乘以 4 倍,以此类推。
文件格式
下面展示了 ANN 和 SVM 分类器的文件格式。投影矩阵也使用线性、两层神经网络实现,分别由 361 和 40 个神经元组成,因此你可以使用你自己的投影基以该格式。
我将以 skin.nn 文件为例描述 ANN 文件结构。
3
3 6 1
0
1
0.000000 1.000000
0.000000 1.000000
0.000000 1.000000
12.715479
3.975381
7.267306
-2.828213
-5.901746
56.047752
-31.722653
-21.872528
-10.393643
-11.324373
42.869545
-27.680447
3.026848
-8.795427
-32.185852
62.505981
8.211640
-32.108111
81.444914
-48.067621
8.276753
19.659469
-50.115191
27.516311
-15.402503
-9.760841
4.400588
18.516097
0.624907
4.414670
17.972277
3 是层数。3 6 1 是每层的神经元数量。接下来的两个数字 0 和 1 表示输入层的线性归一化,1 表示隐藏层和输出层的 sigmoid 压缩。接下来的两列零和一分别是输入向量的归一化参数。第一列是加项,第二列是乘项。因此,输入到网络的每个输入向量都根据以下公式进行归一化:
X[i] = (X[i] + A[i]) * B[i];
接下来是神经元系数本身。你可以使用 ANNetwork::saveW()
函数获取矩阵。
W1 4x6
12.7155 -5.9017 -10.3936 3.0268 8.2116 8.2768
3.9754 56.0478 -11.3244 -8.7954 -32.1081 19.6595
7.2673 -31.7227 42.8695 -32.1859 81.4449 -50.1152
-2.8282 -21.8725 -27.6804 62.5060 -48.0676 27.5163
W2 7x1
-15.4025
-9.7608
4.4006
18.5161
0.6249
4.4147
17.9723
每一列是一个神经元的权重,第一个元素是偏置权重,输入为 1.0。只需按列顺序连接它们,即可得到我的文件格式。
这是 4 层网络的示例。
4
3 8 4 1
0
1
0.000000 1.000000
0.000000 1.000000
0.000000 1.000000
0.025145
-0.235785
0.251309
0.746412
-0.554480
-0.608912
0.592695
-0.035175
-0.471400
-0.362423
-0.010304
0.264203
0.954675
0.097983
0.960209
-0.744931
-0.008009
-1.698955
-0.697944
1.098258
0.500820
-0.199090
-0.157125
1.243531
-0.411520
-1.217646
-0.388029
0.768772
-0.364904
-1.983003
0.663086
1.999845
1.052666
0.241385
0.301041
-0.479706
1.353455
-1.687823
-0.411694
-1.080923
-1.886460
0.517472
-0.873151
0.833913
-0.836865
0.143565
0.822298
0.750636
0.407058
0.601209
0.802379
-0.277260
0.334823
0.086736
0.254606
-0.617788
-0.544399
-0.724927
-1.301591
-1.319710
0.708224
0.845066
0.172427
-0.606166
0.311338
0.405023
0.610455
1.672150
-0.192386
-2.995480
1.061938
-1.674936
2.250647
其权重矩阵如下所示,以便你有一个大致的了解。
W1 4x8
0.0251 -0.5545 -0.4714 0.9547 -0.0080 0.5008 -0.4115 -0.3649
-0.2358 -0.6089 -0.3624 0.0980 -1.6990 -0.1991 -1.2176 -1.9830
0.2513 0.5927 -0.0103 0.9602 -0.6979 -0.1571 -0.3880 0.6631
0.7464 -0.0352 0.2642 -0.7449 1.0983 1.2435 0.7688 1.9998
W2 9x4
1.0527 0.5175 0.8024 -1.3197
0.2414 -0.8732 -0.2773 0.7082
0.3010 0.8339 0.3348 0.8451
-0.4797 -0.8369 0.0867 0.1724
1.3535 0.1436 0.2546 -0.6062
-1.6878 0.8223 -0.6178 0.3113
-0.4117 0.7506 -0.5444 0.4050
-1.0809 0.4071 -0.7249 0.6105
-1.8865 0.6012 -1.3016 1.6722
W3 5x1
-0.1924
-2.9955
1.0619
-1.6749
2.2506
SVM 的格式也很简单。
361
2
linear
-0.806308
1
... 361 entries of numbers
-1
... 361 entries of numbers
361 是输入向量的维度,2 是文件中存在的支持向量数量,linear 是 SVM 类型,-0.806308 是偏置,然后是实际的支持向量。本例中的第一个元素 1 表示第一个 SV,-1 表示第二个,代表权重,即 alpha * y,其中 y 是类标记 -1 或 1,alpha 是你从 SVM 训练中获得的。在我的例子中,我使用了线性 SVM,并将支持向量作为人脸类别和非人脸类别均值向量。有关训练公式的描述,请参阅 B. Scholkopf 的《Statistical Learning and Kernel Methods》。我记得我从 Kernel Machines 网站下载了它。
一些结果
在这里,我展示了一些我刚收集到的应用程序截图。你可以查看底部状态栏中的 fps 率。Motion and skin 表示检测到的比例,范围为 0.0-1.0。



关注点
随着多核处理器的出现,你可以使用 OpenMP 支持来优化 vec2D
类。我相信这将使检测速度提高几倍。你还可以尝试不同的神经网络拓扑结构,或尝试使用更少的特征向量,还可以尝试在线数据库。如果你获得更好的结果,请告知我。我将把你的分类器作为插件发布。
更新
2008年1月9日 - 在 CBCL 人脸数据库上重新训练了分类器
CBCL 人脸数据集包含 2429 个 19x19 尺寸的人脸样本和 4548 个非人脸样本。我将它们添加到我从几个人那里收集的样本中,并在这样新的组合数据集上重复了 PCA 和分类器训练。我执行的必要步骤是对 CBCL 数据进行高斯平滑和直方图均衡化,因为我在算法中使用了这些操作作为预处理步骤。我相信,现在那些面部特征与我差异很大的人也能被检测出来。
新的 40 个特征向量如下所示。

它们也包含约 95% 的方差。投影数据如下所示,表示为均值 ± 标准差。红色表示人脸数据,绿色表示非人脸。我也减少了我的非人脸数据。

你可以看到,第 4 个投影提供了最佳的类别分离。第 4 个特征向量类似于某些人脸。
我使用了我的 Backpropagation Artificial Neural Network in C++ 并再次进行了训练,其中 50% 用于训练集,50% 用于验证和测试集。
ann1Dn.exe t pca19x19.nn scrpca void 100 void void 0.5 3
TRAINING SET: 12977
VALIDATION SET: 6654
TEST SET: 6987
loading data...
cls1: 4217 cls2: 22401 files loaded. size: 40 samples
validaton size: 1054 5600
test size: 1107 5880
training...
epoch: 1 out: 0.845651 0.141380 max acur: 0.90 (epoch 1)
se:97.25 sp:96.52 ac:96.63
epoch: 2 out: 0.884783 0.106216 max acur: 0.92 (epoch 2)
se:97.72 sp:97.23 ac:97.31
epoch: 3 out: 0.895387 0.097048 max acur: 0.92 (epoch 2)
se:97.72 sp:97.23 ac:97.31
epoch: 4 out: 0.900517 0.093550 max acur: 0.94 (epoch 4)
se:98.01 sp:97.82 ac:97.85
epoch: 5 out: 0.903232 0.090293 max acur: 0.94 (epoch 5)
se:96.87 sp:98.23 ac:98.02
epoch: 6 out: 0.905548 0.088409 max acur: 0.94 (epoch 5)
se:96.87 sp:98.23 ac:98.02
epoch: 7 out: 0.902383 0.086369 max acur: 0.94 (epoch 7)
se:97.15 sp:98.38 ac:98.18
epoch: 8 out: 0.904038 0.084748 max acur: 0.94 (epoch 7)
se:97.15 sp:98.38 ac:98.18
epoch: 9 out: 0.905107 0.081640 max acur: 0.94 (epoch 7)
se:97.15 sp:98.38 ac:98.18
epoch: 10 out: 0.902668 0.081184 max acur: 0.94 (epoch 7)
se:97.15 sp:98.38 ac:98.18
epoch: 11 out: 0.901597 0.079696 max acur: 0.95 (epoch 11)
se:96.49 sp:98.64 ac:98.30
training done.
training time: 00:00:47:531
classification results: pca19x19.nn
train set: 2056 10921
sensitivity: 99.81
specificity: 99.60
+predictive: 97.90
-predictive: 99.96
accuracy: 99.63
validation set: 1054 5600
sensitivity: 97.15
specificity: 98.73
+predictive: 93.52
-predictive: 99.46
accuracy: 98.48
test set: 1107 5880
sensitivity: 98.01
specificity: 98.45
+predictive: 92.26
-predictive: 99.62
accuracy: 98.38
与在我自己的数据上训练相比,在新组合数据集上的性能下降了约 1-3%。
线性 SVM 预过滤器由 2 个支持向量组成,代表原始 19x19 人脸和非人脸数据的均值,但分类性能显著提高。
sensitivity: 81.65
specificity: 91.09
+predictive: 63.31
-predictive: 96.35
accuracy: 89.60
现在,你可以解压 pca.nn、face.nn、preflt.nn 文件,并用它们替换我旧的 AI 文件(也要备份我之前的)。现在运行程序。记住,如果光照条件不好,请删除 skin.nn。如果你仍然未被检测到,请删除 SVM preflt.nn 预过滤器(不要忘记重新点击检测复选框以重新加载分类器)。我注意到 SVM 预过滤器在我的某些训练图片上看不到我。这不是问题,因为它是线性的并且非常简单,但它极大地提高了计算性能。我将提供 ANN 预过滤器在新数据集上,在此之前,你可以使用 demo.zip 文件中的旧版本 bin\pca\preflt.nn。