第八章 采用PCA(主成分分析)或LDA(线性判别分析)的人脸识别(二)

【原文:http://blog.csdn.net/raby_gyl/article/details/12623539】

注释:

1、翻译书名:Mastering OpenCV with Practical Computer Vision Projects

2、翻译章节:Chapter 8:Face Recogition using Eigenfaces or Fisherfaces

3、电子书下载,源代码下载请参考:http://blog.csdn.net/raby_gyl/article/details/11617875


上接:第八章 采用PCA(主成分分析)或LDA(线性判别分析)的人脸识别(一):http://blog.csdn.net/raby_gyl/article/details/12611861

转载请注明:http://blog.csdn.net/raby_gyl/article/details/12623539


用收集的人脸训练人脸识别系统


对于每个人搜集了足够的人脸用来识别之后,你必须训练使用一个适合人脸识别的机器学习算法来学习这些数据。在文献中有很多人脸识别算法,其中最简单的就是Eigenfaces和人工神经网络。Eigenfaces通常比人工神经网络要好,而且尽管它简单,它几乎和一些复杂的人脸识别算法一样好。因此对于初学者和作为新的算法的比较,它是非常流行的基本人脸识别算法。


对任何想要在人脸识别从事更长远的读者来说,建议读一下下面的理论:


1、Eigenfaces(也称作主成分分析PCA)

2、Fisherfaces(也称作线性判别分析LDA)

3、其他典型的人脸识别算法(许多可以通过网址访问到:http://www.face-rec.org/algorithms/)

4、最新的人脸识别算法在最近的计算机视觉研究论文上(例如CVPR和ICCV:htpp://www.cvpapers.com/)因为那里每年都有数百人脸识别的文章。

然而,在本书中展示的那样,为了使用它们,你不需要理解这些算法的理论。多谢OpenCV团队和Philipp wagner’s libfacerec贡献。OpenCV v2.41提供了cv::Algorithm作为一个简单的,一般的方法,不需要理解它们是怎样实现的。你可以通过使用Algorighm::getList()函数来找到你的OpeCV视觉可以利用的算法。代码如下:

[cpp]  view plain copy
  1. vector<string> algorithms;  
  2. Algorithm::getList(algorithms);  
  3. cout << "Algorithms: " << algorithms.size() << endl;  
  4. for (int i=0; i<algorithms.size(); i++) {  
  5. cout << algorithms[i] << endl;  
  6. }  

OpenCV v2.4.1可以使用的三个人脸识别算法,如下:

1、FaceRecognizeer.Eigenfaces:Eigenfaces,也叫做PCA,首先被Turk和Pentland在1991年使用.

2、FaceRecognizeer.Fisherfaces:Fisherface人脸.也叫做LDA。Belhumeur, Hespanha and Kriegman in 1997发明。

3FaceRecognizer.LBPH:局部二值化类型直方图,Ahonen, Hadid and Pietikäinen in 2004发明。


注释:

更多人脸识别算法的实现,可以在文档,例子中找到,对于Python平台同等的可以在 Philipp Wagner的网站上找到:http://bytefish.de/ blog and http://bytefish.de/dev/libfacerec/.


通过OpenCV 的contrib模块的FaceRecognizer类可以利用这些人脸识别的算法。由于是动态链接,你的程序需要链接contrib模块.但是它并不是实时加载的(如果被认为不需要)。因此推荐在试图访问FaceRecognizer算法之前,调用cv::initModule_contrib()函数。这个函数只能在OpenCV 2.4.1以上的版本访问的到,因此它至少确保人脸识别算法通过你的编译时间(即不会报编译时错误):

[cpp]  view plain copy
  1. //在运行时动态的导入"contrib"模块  
  2. bool haveContribModule = initModule_contrib();  
  3. if (!haveContribModule) {  
  4. cerr << "ERROR: The 'contrib' module is needed for ";  
  5. cerr << "FaceRecognizer but hasn't been loaded to OpenCV!";  
  6. cerr << endl;  
  7. exit(1);  
  8. }  

为了使用人脸识别算法的一个,我们必须使用cv::Algorithm::create<FaceRecognizer>()函数创建一个FaceRecognizer对象,我们传递我们想使用的人脸识别算法的名字,作为创建函数的string对象。如果它在OpenCV的版本中有效,我们将通过它访问那个算法。确保用户的OpenCVv2.4.1或者更新的版本,它将在运行时被检查。例如:

[cpp]  view plain copy
  1. string facerecAlgorithm = "FaceRecognizer.Fisherfaces";  
  2. Ptr<FaceRecognizer> model;  
  3. //使用”contrib”模块的人脸识别器  
  4. model = Algorithm::create<FaceRecognizer>(facerecAlgorithm);  
  5. if (model.empty()) {  
  6. cerr << "ERROR: The FaceRecognizer [" << facerecAlgorithm;  
  7. cerr << "] is not available in your version of OpenCV. ";  
  8. cerr << "Please update to OpenCV v2.4.1 or newer." << endl;  
  9. exit(1);  
  10. }  

一旦我们导入了人脸识别算法,我们可以简单地用我们的人脸数据调用人脸识别FaceRecognizer::train()函数,如下:

[cpp]  view plain copy
  1. // 用收集的人脸做实际的训练  
  2. model->train(preprocessedFaces, faceLabels)  


这行一行代码将运行你选择的一整套的人脸识别训练算法。(例如,EigenfacesFisherfaces,或者其他可能的算法)。如果你仅有少于20个人的人脸,那么这个算法将返回的很快,但是如果你有很多人的人脸,这个train()函数可能花费几秒或者甚至几分钟来处理所有的数据。


查看学习的知识


然而并不是必须的,在学习你的训练数据时,观察人脸识别算法产生的内部数据结果是相当有用的,特别地如果你想理解你选择的算法背后的理论,并且想核实它是否工作或者找到为什么他不像你期望的那样工作。对于不同的算法内部数据结构可能不同,但是幸运的是,对于EigenfacesFisherfaces他们是相同的,因此让我们仅看看这两个。这两个都是基于一维特征矢量矩阵,当他们作为二维图像观察时,表现的有点像人脸。因此当使用人脸识别算法,Eigenfaces或者Fisherfaces时,将特征矢量作为特征人脸是很普通的。


在简单的术语中,Eigenfaces的基本原理是,将计算一组特殊图像(特征脸eigenfaces)并且混合比例(特征值),在训练集合中以不同的方式混合可以产生训练图像集中的每一个图像,但是同样可以用于在训练集中区分许多人脸图像。例如:在训练集中的一些人脸带有小胡子,一些没有,那么至少有一个特征脸来表现小胡子。并且带有小胡子的训练人脸应当有一个高的混合比例来表现它带有一个小胡子。并且不带小胡子的人脸对于他们的特征向量有一个低的混合比例。如果一个训练集有5个人,对于每个人有20张人脸,那么在训练集中将会100个特征人脸和特征矢量来区分100全部人脸,并且事实上这些将被排序,因此第一个少量的特征脸和特征矢量将会是最重要的区分器,并且最后一个少量的特征脸和特征矢量仅是随机的像素噪声,对我们区分数据实际上没有帮助。因此很自然的抛弃最后的一些特征并且仅保存前50个左右的特征人脸。


相比较,Fisherfaces的基本原理是,代替计算训练集中每个图像的特征向量和特征值,它只计算每一个人的一个特征向量和特征值,因此在前面的5个人,每人20个人脸的例子中,Eigenfaces算法将使用100特征人脸和特征值,然而Fisherfaces算法将使用仅5个特征人脸和特征值。


为了访问EigenfacesFisherfaces算法的内部数据结构,我们使用cv::Algorithm::get()函数在运行时获得他们,因为在编译时,无法访问他们。数据结构作为数学计算的一部分而不是图像处理的一部分来使用。因此他们通常以浮点型数据存储,典型的在0.01.0范围内。而不是从02558uchar类型的像素,在整齐的图像中,类似于像素。他们通常也是一维行或者列矩阵或者组成有很多一维行向量或者列向量的矩阵。因此在显示这些内部数据结构之前,你必须将他们的转换成正确的矩形形状(reshape),把他们转换到0255范围内的8uchar类型像素。因为矩阵数据可能在0.01.0或者-1.01.0或者任何范围之内。你可以使用cv::normalize()函数,带有cv::norm_minmax可选参数,来确保输出的数据在0255之间,而不管输入数据的范围是什么。让我们创建函数来执行到矩形形状转变以及到8bit像素的转换,如下:

[cpp]  view plain copy
  1. //转换行向量或者列向量(float类型)到一个8维矩形图像,能被用来显示和存储  
  2. //尺度化值到0和255之间  
  3. Mat getImageFrom1DFloatMat(const Mat matrixRow, int height)  
  4. {  
  5. // 转换为矩形形状图像,用来代替单行  
  6. Mat rectangularMat = matrixRow.reshape(1, height);  
  7. //尺度化到0和255之间,并将他们存储为8位uchar图像  
  8. Mat dst;  
  9. normalize(rectangularMat, dst, 0, 255, NORM_MINMAX, CV_8UC1);  
  10. return dst;  
  11. }  

为了易于调试 opencv 代码,更甚至当内部调试 cv::Algorithm 数据结构时,我们可以使用 ImgeUtils.Cpp ImageUtils.h 文件来简单地显示有关 cv::Mat 结构信息,如下:

[cpp]  view plain copy
  1. Mat img = ...;  
  2. printMatInfo(img, "My Image");  

你将看到类似如下的信息打印到你的控制台:

[cpp]  view plain copy
  1. My Image: 640w480h 3ch 8bpp, range[79,253][20,58][18,87]  

这告诉你它是一个640个元素宽和480个元素高(即640*480图像或者一个480*640的矩阵,依赖于你么看它),带有8位三通道图像(即普通的BGR图像),并且它展示了图像的每个通道的最小值和最大值。


注释:

1、也可能使用printMat()函数替代printMatInfo()函数来打印图像或者矩阵的实际的内容。这可以非常方便的查看矩阵和多通道浮点型矩阵,因为这些可能对于一个初学者是相当棘手的。

2、ImageUtils代码主要为OpenCV的接口,但是逐渐的包含更多的C++借口。最新的版本可以在网站http://shervinemami.info/openCV.html上找到。


人脸均值


Eigenfaces和Fisherfaces算法都是先计算人脸均值即所有训练图像的人脸均值。因此他们可以从每个人脸图像减去均值图像得到更好的识别结果。因此让我们看一下我们训练集的人脸均值。人脸均值在EigenfacesFisherfaces实现中称为mean,展示如下:

[cpp]  view plain copy
  1. Mat averageFace = model->get<Mat>("mean");  
  2. printMatInfo(averageFace, "averageFace (row)");  
  3. //转换一个浮点行矢量到一个常规的8位图像  
  4. averageFace = getImageFrom1DFloatMat(averageFace, faceHeight);  
  5. printMatInfo(averageFace, "averageFace");  
  6. imshow("averageFace", averageFace);  


现在你应该在你的屏幕上看到一个均值人脸图像,类似与下面的放大的图像,它是一个男人,一个女人和一个孩子的组合。你应当也能在你的控制台看到类似的文本展示:

[cpp]  view plain copy
  1. averageFace (row): 4900w1h 1ch 64bpp, range[5.21,251.47]  
  2. averageFace: 70w70h 1ch 8bpp, range[0,255]  

图像的显示像下面的截图:


注意到averageFace(row)是一个单行64位浮点型矩阵,然而averageFace是一个覆盖从02558位的矩形图像。



特征值, Eigenfaces, and Fisherfaces


让我们看一下实际的特征值的组成:

[cpp]  view plain copy
  1. Mat eigenvalues = model->get<Mat>("eigenvalues");  
  2. printMat(eigenvalues, "eigenvalues");  

对于Eigenfaces,对于每一个人脸都有一个特征值,因此如果我们有三个人,每个人带有4张人脸,我将得到一个带有12特征值的列矢量,按照从最好到最坏排序,如下:

[cpp]  view plain copy
  1. eigenvalues: 1w18h 1ch 64bpp, range[4.52e+04,2.02836e+06]  
  2. 2.03e+06  
  3. 1.09e+06  
  4. 5.23e+05  
  5. 4.04e+05  
  6. 2.66e+05  
  7. 2.31e+05  
  8. 1.85e+05  
  9. 1.23e+05  
  10. 9.18e+04  
  11. 7.61e+04  
  12. 6.91e+04  
  13. 4.52e+04  

对于Fisherfaces人脸,对每一个附加的人我们仅有一个特征值,因此如果有三个人,每个人有4张人脸,我们仅得到一个带有2个特征值的行矢量,如下:

[cpp]  view plain copy
  1. eigenvalues: 2w1h 1ch 64bpp, range[152.4,316.6]  
  2. 317, 152  


为了观察特征矢量(例如Eigenfaces或者fisherface图像)。我们必须从大的特征矢量矩阵中抽取他们作为列。因为opencv的和c/c++中的数据通常以行顺序存储在矩阵中的。这意味着为了抽取一列,我们应当使用Mat::clone()函数来确保数据将是连续的,否则我们不能将数据的形状改变成为矩形(reshape)。一旦我们有了一个连续的列Mat.我们可以使用getImageFrom1DFloatMat()函数就像前面我们为均值人脸所做的那样来显示特征矢量:

[cpp]  view plain copy
  1. // 获取特征矢量  
  2. Mat eigenvectors = model->get<Mat>("eigenvectors");  
  3. printMatInfo(eigenvectors, "eigenvectors");  
  4. //展示最好的20个特征矢量  
  5. for (int i = 0; i < min(20, eigenvectors.cols); i++) {  
  6. //从特征矢量创建一个连续的列矢量 #i.  
  7. Mat eigenvector = eigenvectors.col(i).clone();  
  8. Mat eigenface = getImageFrom1DFloatMat(eigenvector,  
  9. faceHeight);  
  10. imshow(format("Eigenface%d", i), eigenface);  
  11. }  

下面的图像显示了特征矢量图像,你可以看到三个人,每个人带有4张人脸,有12Eigenfaces(图像的左手边)或者两个Fisherfaces人脸(图像右手边)。(这里是20个Eigenfaces)

第八章 采用PCA(主成分分析)或LDA(线性判别分析)的人脸识别(二)_第1张图片


注意EigenfacesFisherfaces好像有一些相似的人脸特征,但是他们看起来不像人脸。这是简单的因为均值图像从他们中减去了,因此他们仅仅是从均值人脸中展示每个特征人脸的不同。数字表示是哪个特征人脸,因为他们已经按照从最重要的特征到最不重要的特征人脸进行了排序。并且如果你有50或者更多的特征人脸,那么后面的特征人脸经常仅仅显示随机的图像噪声,并且因此应当被抛弃。


步骤4:人脸识别


既然我们已经用训练图像和人脸标签集训练了Eigenfaces 或者Fisherfaces的特征学习算法。我们最终准备仅从一个张人脸图像中指出那个人是谁。这一步涉及到人脸识别或者人脸鉴定。


(注意下面要将的人脸鉴定(identification)和人脸验证(verification)的区别)

人脸鉴定(identification):从他们的人脸识别一个人


多谢OpenCVFaceRecoginer类,我们能可以简单地在人脸图像上调用FaceRecognizer::predit()函数来实现识别图像中的人,如下:

[cpp]  view plain copy
  1. int identity = model->predict(preprocessedFace);  


这个identity值是我们最初在搜集人脸训练时的标签数据,0表示第一个人,1表示第二个人。等等。这种识别的问题是,它总是预测一个给定的人,即使输入图像是一个未知的人或者一个车。它任然告诉你图像中的那个人更像哪一个,因此很难相信结果。解决的方法是获得一个信任度量,因此我们能够判断结果的可靠程度。并且如果它看起来信任度很低,那么我们假定它是一个未知的人。


人脸验证(verification) :断定是一个人

为了证实预测(prediction)的结果是可靠的或者它应当被认为一个未知的人,我们进行人脸验证(也即人脸认证),为了获得一个信任度量来表示是否单个人脸图像相似与断言的人脸(相对应我们刚刚进行的人脸鉴定来说,用单个人脸和很多人比较)。


当我们调用predict()函数时,OpenCVFaceRecogizer类能够返回一个信任度量。但是不幸地信任度量简单的依赖于到特征子空间的距离,因此它不是很可靠。我们将要使用的方法是用特征向量和特征值来重构人脸图像,并且将重构的图像和输入的图像比较。如果一个人在训练集中有很多人脸,那么通过学习特征向量和特征值可能得到很好的重构效果。但是如果一个人在训练集中没有人脸(或者没有在测试图像中类似的光照和人脸表情),那么重构的人脸将和输入的人脸有很大的不同。标志着它可能是一个未知的人脸。


记住我们之前所说的,EigenfacesFisherfaces是基于这个一个概念:图像可以近似地表示为一个特征向量(特殊的人脸图像)和特征值(混合比例)。如果我们用特征值组合训练集中所有特征矢量,那么我们将得到一个相当近似的源、原训练图像的一个副本。同样的应用类似与其他图片,他们类似于训练集。如果我们使用一个相似的测试图像的特征值来组合训练的特征矢量,我们将可能重构图像,在某种程度上是测试图像的一个副本。


再次,OpenCV的人脸识别类使得很容易的从输入图像中产生一个重构的图像,采用subspaceProject函数映射到一个特征空间,使用subspaceReconstruct()函数从特征空间返回到图像空间。诀窍是我们需要将它从一个浮点型行矩阵转换为矩形8位图像(就像我们显示均值人脸和特征人脸那样),但是我们不想归一化这些数据,因为用来和原图像相比较,它已经是一个理想的尺度。如果我们归一化了数据,它可能和输入图像有不同的光照和对比度。仅仅使用L2相关误差来比较图像的相似度将会变的困难。做法如下:

[cpp]  view plain copy
  1. //从FaceRecognizer的model对象中获取一些需要的数据  
  2. Mat eigenvectors = model->get<Mat>("eigenvectors");  
  3. Mat averageFaceRow = model->get<Mat>("mean");  
  4. //映射输入图像到特征空间  
  5. Mat projection = subspaceProject(eigenvectors, averageFaceRow,preprocessedFace.reshape(1,1));  
  6.   
  7. //从特征子空间产生一个重构的人脸  
  8. Mat reconstructionRow = subspaceReconstruct(eigenvectors,averageFaceRow, projection);  
  9.   
  10. //使其变成一个矩形图像来代替一个单行  
  11. Mat reconstructionMat = reconstructionRow.reshape(1, faceHeight);  
  12.   
  13. // 将float图像转换为8为图像  
  14. Mat reconstructedFace = Mat(reconstructionMat.size(), CV_8U);  
  15. reconstructionMat.convertTo(reconstructedFace, CV_8U, 1, 0);  


下面的图像展示了两个典型的重构人脸,左手边人脸是好的重构,因为它来至于一张已知的人脸,然后右手边是坏的重构,因为它来至于一张未知的人,或者一个已知的人但是未知的光照/表情/人脸方向.

第八章 采用PCA(主成分分析)或LDA(线性判别分析)的人脸识别(二)_第2张图片


我们现在可以同样使用我们先前为比较两个图像创建的getSimilarity()函数来计算输入图像和重构图像的相似度,先前similarity值低于0.3就表示两个图像非常相似。对于Eigenfaces,每一张脸都有一个特征矢量,因此重建趋向于好的结果,因此我们可以典型地使用0.5的阈值。但是 Fisherfaces 对于每一个人仅有一个特征矢量,因此重建并不是很好,因此它需要一个更高的阈值—0.7。做法如下:

[cpp]  view plain copy
  1. similarity = getSimilarity(preprocessedFace,   
  2. reconstructedFace);  
  3. if (similarity > UNKNOWN_PERSON_THRESHOLD) {  
  4. identity = -1; // Unknown person.  
  5. }  

现在你可以仅打印identity标识到控制台了,或者使用到你想使用的任何的地方。记住这个人脸识别方法和人脸验证方放仅在你为它训练的特定的环境下可靠。因此为了获取好的识别精度,你将需要确保你的每一个人的训练集覆盖全部的光照范围,人脸表情和角度,即你期待的测试情况。人脸处理阶段帮助减少光照和面内旋转的(如果一个人倾斜他的头到左肩或者右肩)差异。但是对于其他差异例如平面外旋转(如果这个人将他的头朝向左手边或者手右边)。它只有在包含在你的训练集中才有效。


最后一笔:保存和导入文件


你可能潜在地增加一个命令行,来处理输入文件夹和保存他们到磁盘。或者执行人脸检测,人脸处理和(或)人脸识别作为一个网络服务,等等。对于这些类型的工程,通过使用FaceRecoginzer类的saveload函数可以很容易的添加想要的功能。你可能也希望保存训练数据,并且然后在工程开始使导入它。


保存训练模型到XML或者YML文件很容易:

[cpp]  view plain copy
  1. model->save("trainedModel.yml");  

如果你想以后增加更多的数据到训练集,你可能也想保存一组预处理的人脸和标签。

例如,这里有一些样本代码用来从文件夹中导入训练模型。注意你必须指定人脸识别算法(例如,FaceRecoginzer.Eigenfaces或者FaceRecoginzer.Fisherfaces),这是最初用来创建训练模型的:

[cpp]  view plain copy
  1. string facerecAlgorithm = "FaceRecognizer.Fisherfaces";  
  2. model = Algorithm::create<FaceRecognizer>(facerecAlgorithm);  
  3. Mat labels;  
  4. try {  
  5. model->load("trainedModel.yml");  
  6. labels = model->get<Mat>("labels");  
  7. catch (cv::Exception &e) {}  
  8. if (labels.rows <= 0) {  
  9. cerr << "ERROR: Couldn't load trained data from "  
  10. "[trainedModel.yml]!" << endl;  
  11. exit(1);  
  12. }  


最后一笔:做一个漂亮的和互动的GUI


虽然到目前为止在这一章给出的代码对于一个人脸识别系统已经足够了,但仍需要一个方式来将数据输入系统和一个方式来使用它。许多用来研究的人脸识别系统将选择理想的输出到文本文件夹,这里文本列出静态图像文件夹在计算机的存储位置,和其他重要的数据例如一个人的真实名字或者身份并且可能人脸区域真实的像素坐标。(例如人脸和眼睛中心位置的实况)。这可能或者被另外一个人脸识别系统手工地收集。


理想的输出可能将是一个文本文件来比较识别结果和真实情况,因此统计数字可能通过人脸识别系统和其他人脸识别系统的比较来获得。


然而,因为人脸识别系统在这一章被设计用来学习和可用的有趣的目的,而不是与最新的研究方法竞争。有一个简单的,可用的GUI来做人脸识别,训练和测试是有用的,并且实时的来至网络摄像机的交互。因此,在这一部分将提供一个交互的GUI来为服务这些特征(个人理解,来服务人脸识别,训练)。读者或者期望使用本书提供的GUI,或者为他们自己的工程修改GUI,或者忽略这个GUI并且设计自己的GUI来执行到目前为止讨论的人脸识别技术。


由于我们需要GUI来执行多个任务,让我们创建一个GUI将要使用的模式集(modes)或者状态集(states),用户使用按键或者点击鼠标来改变模式:

1、 Startup 这个模式导入、初始化数据和相机。

2、Detection这个模式检测人脸并且显示预处理,直到用户点击增加人脸( Add Person)的按钮

3、Collection:这个模式收集当前人脸,直到用户点击窗口的任何地方。这也显示每个人最近人脸。用户或者点击一个存在的人脸或者点击增加人脸 Add Person按钮,来为不同的人收集人脸。

4、Training:

在这一模式,系统用所有收集的人的人脸来训练.

5、Recognition

 由标记识别的人和展示一个可信尺度组成。可以或者点击其中一个人或者点击增加人按钮,来返回模式 2 Collection)。


为了退出,用户可以在任何时候点击窗口的退出。让我们增加一个删除所有的模式来重启一个人脸识别系统,并且增加一个调试按钮来触发额外调试信息的显示。我可以创建一个枚举类型的模式变量来显示当前的模式。


画出GUI元素


为了在屏幕上显示当前模式,我们创建一个函数来简单地画出文本。OpenCV带有cv::putText()函数,该函数带有几个字体和抗锯齿。但是将文本显示在你想显示的位置是复杂的。幸运地是,同样有一个cv::getTextSize()函数用来计算文本的边界框,因此我们创造一个封装函数来更容易的显示文本。我们想在窗口的任何边显示文本并且确保它完全可见,并且也允许文本的多行或者字紧挨着彼此不被覆盖。因此这里的封装函数允许你指定左侧调整或者右侧调整,同样也可以指定上侧调整或者下侧调整,并且返回一个边界框。因为我们可以容易地在窗口的角或者边界画出多行的文本。

[cpp]  view plain copy
  1. //在图像上画文本,默认为左上调整文本,因此为右侧调整文本给出负x坐标,并且/或者为底侧调整给出负y坐标  
  2. //返回所画文本的边界矩形  
  3. Rect drawString(Mat img, string text, Point coord, Scalar   
  4. color, float fontScale = 0.6f, int thickness = 1,  
  5. int fontFace = FONT_HERSHEY_COMPLEX);  


现在为了在GUI显示当前的模式,因为窗口的背景是有相机进入,如果我们简单在相机流中画文本,这是相当可能的,它可能与相机背景同样的颜色!因此让我们画一个黑色的文本阴影,它与我们想要画的前景的文本仅有一个像素之差。让我们在他的下面画一个有用的文本行。因此用户知道跟随着步骤。下面是一个怎样使用drawString函数来画一些文本的例子。

[cpp]  view plain copy
  1. string msg = "Click [Add Person] when ready to collect faces.";  
  2. // 画一个黑色阴影,再画一个白色文本  
  3. float txtSize = 0.4;  
  4. int BORDER = 10;  
  5. drawString(displayedFrame, msg, Point(BORDER, -BORDER-2),CV_RGB(0,0,0), txtSize);  
  6. Rect rcHelp = drawString(displayedFrame, msg, Point(BORDER+1,-BORDER-1), CV_RGB(255,255,255), txtSize);  

下面的部分截图展示了模式和GUI窗口底部信息,覆盖在相机图像上:



我们提到,我们想要少量的GUI按钮,因此,让我们简单地画出GUI按钮,如下:

[cpp]  view plain copy
  1. //用drawString()函数,在图像上画一个GUI按钮  
  2. // 可以设置minWidth参数,来使几个按钮具有同样的宽度  
  3. //返回所画按钮的边界矩形  
  4. Rect drawButton(Mat img, string text, Point coord,  
  5. int minWidth = 0)  
  6. {  
  7. const int B = 10;  
  8. Point textCoord = Point(coord.x + B, coord.y + B);  
  9. //获取文本的边界框   
  10. Rect rcText = drawString(img, text, textCoord, CV_RGB(0,0,0));  
  11. //在文本周围画一个填充的矩形  
  12. Rect rcButton = Rect(rcText.x - B, rcText.y – B,rcText.width + 2*B, rcText.height + 2*B);  
  13. // 设置按钮的最小宽度  
  14. if (rcButton.width < minWidth)  
  15. rcButton.width = minWidth;  
  16. //创建一个半透明的白色矩形  
  17. Mat matButton = img(rcButton);  
  18. matButton += CV_RGB(90, 90, 90);  
  19. // 画一个不透明的边界  
  20. rectangle(img, rcButton, CV_RGB(200,200,200), 1, CV_AA);  
  21. // 画实际要显示的文本  
  22. drawString(img, text, textCoord, CV_RGB(10,55,20));  
  23. return rcButton;  
  24. }  


现在我们使用drawButtion()函数创建了几个可以点击的GUI按钮,这些按钮总是显示在GUI的左上方,就像下面截图展示的:

第八章 采用PCA(主成分分析)或LDA(线性判别分析)的人脸识别(二)_第3张图片


像我们提到的,GUI程序有很多模式,可以在他们之间切换(作为一个有限状态(模式)的机器)。开始使用Startup模式。我们将使用m_mode变量存储当前模式。


Startup模式

Startup模式中,我们仅需要导入XML检测器文件来检测人脸和眼睛以及初始化网络摄像机。这些我们已经涉及到了。让我们用鼠标回调函数来创建一个主要的GUI窗口,当在窗口上用户移动或者点击鼠标时OpenCV将调用该函数。如果相机支持的话,同样需要设置合理的分辨率,例如640*480,做法如下:

[cpp]  view plain copy
  1. //为在屏幕上显示,创建一个GUI窗口   
  2. namedWindow(windowName);  
  3. //当用户点击窗口时调用onMouse()函数.  
  4. setMouseCallback(windowName, onMouse, 0);  
  5. // 设置相机的分辨率,只对某些系统有效  
  6. videoCapture.set(CV_CAP_PROP_FRAME_WIDTH, 640);  
  7. videoCapture.set(CV_CAP_PROP_FRAME_HEIGHT, 480);  
  8. //我们已经初始化,因此让我们开始Detection模式   
  9. m_mode = MODE_DETECTION;  


Detection模式


Detection模式中,我们希望连续地检测人脸和眼睛,画出包围他们矩形或者圆来显示检测的结果,并且显示当前预处理过的人脸。事实上,我们想,不管我们进入哪一个模式,都能显示。仅一个特殊的事情是关于人脸检测,当用户点击增加人按钮时,该模式将转换到下一个模式(收集)。如果你记住了在这章中先前的检测步骤,我们检测阶段的输出将是:

1、Mat preprocessedFace :预处理的人脸(如果人脸和眼睛被检测到)

2、Rect faceRect;检测到的人脸区域坐标

3、Point leftEye.rightEye:检测到的左眼和右眼中心坐标


 

因此我们应当检查是否返回预处理人脸,如果脸和眼被检测到,画出包围他们的矩形和圆。如下:

[cpp]  view plain copy
  1. bool gotFaceAndEyes = false;  
  2. if (preprocessedFace.data)  
  3. gotFaceAndEyes = true;  
  4. if (faceRect.width > 0) {  
  5. // 在检测到的人脸周围画一个平滑的矩形  
  6. rectangle(displayedFrame, faceRect, CV_RGB(255, 255, 0), 2,CV_AA);  
  7. // 为两眼画浅蓝色的圆  
  8. Scalar eyeColor = CV_RGB(0,255,255);  
  9. if (leftEye.x >= 0) { // 检测左眼是否检测到  
  10. circle(displayedFrame, Point(faceRect.x + leftEye.x,faceRect.y + leftEye.y), 6, eyeColor, 1, CV_AA);  
  11. }  
  12. if (rightEye.x >= 0) { // 检测右眼是否检测到  
  13. circle(displayedFrame, Point(faceRect.x + rightEye.x,faceRect.y + rightEye.y), 6, eyeColor, 1, CV_AA);  
  14. }  
  15. }  


我们将当前预处理过的人脸覆盖在窗口的中上处,如下:

[cpp]  view plain copy
  1. int cx = (displayedFrame.cols - faceWidth) / 2;  
  2. if (preprocessedFace.data) {  
  3. //获取人脸的BGR版本,因为输出是BGR  
  4. Mat srcBGR = Mat(preprocessedFace.size(), CV_8UC3);  
  5. cvtColor(preprocessedFace, srcBGR, CV_GRAY2BGR);  
  6. // 获取感兴趣的目的区域  
  7. Rect dstRC = Rect(cx, BORDER, faceWidth, faceHeight);  
  8. Mat dstROI = displayedFrame(dstRC);  
  9. // 复制原图像到目的图像  
  10. srcBGR.copyTo(dstROI);  
  11. }  
  12. //在人脸周围画一个平滑的边界   
  13. rectangle(displayedFrame, Rect(cx-1, BORDER-1, faceWidth+2,faceHeight+2), CV_RGB(200,200,200), 1, CV_AA);  

下面的截图展示了当在检测模式时显示的GUI。预处理过的人脸在中上部显示,并且对检测到的人脸和人眼进行了标记。

第八章 采用PCA(主成分分析)或LDA(线性判别分析)的人脸识别(二)_第4张图片


Collection 模式


像先前提到的,我们限制每秒收集一个人脸并且只要它与先前搜集的人脸有显著的变化。而且要记住,我们不光收集预处理的人脸而且还收集预处理人脸的镜像图像。


在收集模式中,我们想展示每个已知人的最近的人脸并且用户点击这些人中一个人脸来为他们增加更多的人脸或者点击增加人按钮为集增加一个新人。用户必须点击窗口中部一些地方来继续下一个模式(Training)。


 

因此,首先我们需要为每个收集上的人的最新人脸保存一个参考。我们通过更新整形数组m_lastestFace来做到这一点。这个数组仅存每个人索引的排列,这些数据来至于最大的preprocessedFaces数组(即收集所有人的所有人脸)。因为我们也将镜像人脸存储在那个数组,我们想参考倒数第二个人脸,不是最后一个人脸。这个代码应当追加一个添加新人脸(和镜像人脸)到preprocessedFaces数组的代码。如下:

[cpp]  view plain copy
  1. // 为每一个人的最新人脸保存一个参考  
  2. m_latestFaces[m_selectedPerson] = preprocessedFaces.size() - 2;  


当一个新人被增加或者删除时,我们必须总是记住增长和缩小m_lastestFaces数组。(例如,由于用户点击增加人按钮)。现在让我们在窗口的右手边显示每一个搜集上来的人的最近人脸(在Collection模式和后面的Recognition模式中显示)如下:

[cpp]  view plain copy
  1. m_gui_faces_left = displayedFrame.cols - BORDER - faceWidth;  
  2. m_gui_faces_top = BORDER;  
  3. for (int i=0; i<m_numPersons; i++) {  
  4. int index = m_latestFaces[i];  
  5. if (index >= 0 && index < (int)preprocessedFaces.size()) {  
  6. Mat srcGray = preprocessedFaces[index];  
  7. if (srcGray.data) {  
  8. // 获取一个Get a BGR face, since the output is BGR.  
  9. Mat srcBGR = Mat(srcGray.size(), CV_8UC3);  
  10. cvtColor(srcGray, srcBGR, CV_GRAY2BGR);  
  11. // Get the destination ROI  
  12. int y = min(m_gui_faces_top + i * faceHeight,  
  13. displayedFrame.rows - faceHeight);  
  14. Rect dstRC = Rect(m_gui_faces_left, y, faceWidth,  
  15. faceHeight);  
  16. Mat dstROI = displayedFrame(dstRC);  
  17. // Copy the pixels from src to dst.  
  18. srcBGR.copyTo(dstROI);  
  19. }  
  20. }  
  21. }  


我们也想加亮当前被收集的人,使用一个围绕人脸的粗的红色边界。做法如下:

[cpp]  view plain copy
  1. if (m_mode == MODE_COLLECT_FACES) {  
  2. if (m_selectedPerson >= 0 &&m_selectedPerson < m_numPersons) {  
  3. int y = min(m_gui_faces_top + m_selectedPerson * faceHeight, displayedFrame.rows – faceHeight);  
  4. Rect rc = Rect(m_gui_faces_left, y, faceWidth,   
  5. faceHeight);  
  6. rectangle(displayedFrame, rc, CV_RGB(255,0,0), 3, CV_AA);  
  7. }  
  8. }  



下面的部分截图展示了当几个人脸已经被搜集时的典型的显示。

第八章 采用PCA(主成分分析)或LDA(线性判别分析)的人脸识别(二)_第5张图片


Training模式


当用户最终点击窗口的中间,人脸识别算法开始在收集的人脸上进行训练。但是确保有足够的人脸或者人收集是很重要的,否则程序可能崩溃。总的来说,这仅需要确保在训练集中至少有一个人脸。(这暗示至少有一个人)。但是Fisherfaces算法寻找在两个人之间进行比较。因此如果在训练集中少于两个人,程序同样也崩溃。如果是这样,那么,我们至少需要两个人脸,否则我们至少需要一个人的人脸。如果没有足够的数据,那么程序返回收集模式,因此用户可以在训练之前增加更多的人脸。


为了检查是否有至少有两个人收集的人脸,我们可以确保当用户点击增加人按钮时,如果没有任何空的人(也就是说,增加了新人,但是没有任何收集的人脸),一个新人仅增加。当我们正在使用Fisherfaces算法时,那么我们也可以确保是否仅有两个人。因此我们必须确保在收集模式期间相关数组m_latestFaces被设置。当还没有任何人脸增加到增加到那个人时,m_latestFaces[i]被初始化为-1。一旦那个人被添加,它将变为0或者更高。做法如下:

[cpp]  view plain copy
  1. // 检查是否有足够的数据来训练  
  2. bool haveEnoughData = true;  
  3. if (!strcmp(facerecAlgorithm, "FaceRecognizer.Fisherfaces")) {  
  4. if ((m_numPersons < 2) ||  
  5. (m_numPersons == 2 && m_latestFaces[1] < 0) ) {  
  6. cout << "Fisherfaces needs >= 2 people!" << endl;  
  7. haveEnoughData = false;  
  8. }  
  9. }  
  10. if (m_numPersons < 1 || preprocessedFaces.size() <= 0 ||preprocessedFaces.size() != faceLabels.size()) {  
  11. cout << "Need data before it can be learnt!" << endl;  
  12. haveEnoughData = false;  
  13. }  
  14. if (haveEnoughData) {  
  15. // 使用Eigenfaces或者Fisherfaces训练收集的人脸  
  16. model = learnCollectedFaces(preprocessedFaces, faceLabels,facerecAlgorithm);  
  17. //既然训练结束,我们开始识别   
  18. m_mode = MODE_RECOGNITION;  
  19. }  
  20. else {  
  21. // 没有足够的训练数据,返回收集模式  
  22. m_mode = MODE_COLLECT_FACES;  
  23. }  


训练可能不到一秒,或者可能几秒或者甚至几分钟,这取决于收集数据的多少。一旦收集的人脸完成训练。人脸识别系统将要自动地进入识别(Recognition)模式。



Recognition模式


在识别模式中,一个可信尺度挨着预处理的人脸显示,因此用户知道识别的可靠性是怎样的。如果这个信任水平高于未知阈值(unknown threshold)。将在一个识别人的周围画一个绿色的矩形来简单地显示结果。如果用户点击增加人按钮(Add Person)或者已存在的一个人,则可以为进一步训练增加更多的人脸。这将使程序返回到收集(collection)模式。


现在我们已经获得识别的身份和前面提到的重构人脸的相似度。为了显示信任尺度,对于高的信任度L2相似值一般在00.5之间并且对于低的信任值在0.51之间,因此我们可以仅从1中减去它来获得在01之间的信任水平。那么我们仅使用信任水平作为比例来画一个填充的矩形,如下:

[cpp]  view plain copy
  1. int cx = (displayedFrame.cols - faceWidth) / 2;  
  2. Point ptBottomRight = Point(cx - 5, BORDER + faceHeight);  
  3. Point ptTopLeft = Point(cx - 15, BORDER);  
  4. // 对于"unkown" people 画一个灰色线来展示阈值  
  5. Point ptThreshold = Point(ptTopLeft.x, ptBottomRight.y -(1.0 - UNKNOWN_PERSON_THRESHOLD) * faceHeight);  
  6. rectangle(displayedFrame, ptThreshold, Point(ptBottomRight.x,ptThreshold.y), CV_RGB(200,200,200), 1, CV_AA);  
  7. //修剪信任度比率到0和1之间来填充长条  
  8. double confidenceRatio = 1.0 - min(max(similarity, 0.0), 1.0);  
  9. Point ptConfidence = Point(ptTopLeft.x, ptBottomRight.y -confidenceRatio * faceHeight);  
  10. // 展示淡蓝色信任度条  
  11. rectangle(displayedFrame, ptConfidence, ptBottomRight,CV_RGB(0,255,255), CV_FILLED, CV_AA);  
  12. // 画长条的灰色边界.  
  13. rectangle(displayedFrame, ptTopLeft, ptBottomRight,CV_RGB(200,200,200), 1, CV_AA);  

为了加亮识别的人脸,我们在他们的人脸周围画一个绿色的矩形,如下:

[cpp]  view plain copy
  1. if (identity >= 0 && identity < 1000) {  
  2. int y = min(m_gui_faces_top + identity * faceHeight,  
  3. displayedFrame.rows - faceHeight);  
  4. Rect rc = Rect(m_gui_faces_left, y, faceWidth, faceHeight);  
  5. rectangle(displayedFrame, rc, CV_RGB(0,255,0), 3, CV_AA);  
  6. }  


下面的部分截图展示了运行人脸识别模式时一个典型的显示,展示了信任尺度在顶部中心处挨着预处理过的人脸。并且在右上角加亮识别的人脸。

第八章 采用PCA(主成分分析)或LDA(线性判别分析)的人脸识别(二)_第6张图片

检查和处理鼠标点击


既然我们已经把所有的GUI元素画出来了,我们仅需要处理鼠标事件。当我们初始化显示窗口时,我们告诉OpenCV我们想要一个鼠标事件回调给我们的onMouse函数。我们不关心鼠标的运动,只关心鼠标的点击,因此首先我们跳过不是鼠标左键点击的鼠标事件,如下:

[cpp]  view plain copy
  1. void onMouse(int event, int x, int y, intvoid*)  
  2. {  
  3. if (event != CV_EVENT_LBUTTONDOWN)  
  4. return;  
  5. Point pt = Point(x,y);  
  6. ... (handle mouse clicks) ...  
  7. }  

当我们画这些 按钮时,我们获得了这些按钮的矩形边界,我们仅调用OpenCVinside()函数来检测鼠标是否点击了我们按钮区域的任何位置。现在我们可以检测我们创建的每一个按钮。当用户点击增加人按钮(Add Person)时,我们仅增加了一个m_numPersons变量,给变量m_latestFaces分配更多的空间,选择一个新的要收集的人,并且开始收集(Collection)模式。(不管先前我们所在的是哪个模式)。但是有一个混乱;为了确保当训练时,我们拥有每一个人至少一个人脸,如果已没有无人脸数据的人,我们仅为一个新人分配空间。这将确保我们可以总是检查m_lastestFaces[m_numPersons-1]的值来看是否对于每一个人一张人脸已经被收集。做法如下:

[cpp]  view plain copy
  1. if (pt.inside(m_btnAddPerson)) {  
  2. //确保每一个人都带有收集的人脸  
  3. if ((m_numPersons==0) ||(m_latestFaces[m_numPersons-1] >= 0)) {  
  4. // 增加一个新人  
  5. m_numPersons++;  
  6. m_latestFaces.push_back(-1);  
  7. }  
  8. m_selectedPerson = m_numPersons - 1;  
  9. m_mode = MODE_COLLECT_FACES;  
  10. }  


这个方法可以用于测试其他按钮的点击,例如触发debug标志如下:

[cpp]  view plain copy
  1. else if (pt.inside(m_btnDebug)) {  
  2. m_debug = !m_debug;  
  3. }  


为了处理删除所有( Delete All)按钮,我们需要清除我们循环中各种数据结构。(也就是说,无法访问鼠标事件回调函数。)因此我们改变删除所有模式。并且然后我们从内部的主循环中删除所有的事情。我们也必须处理用户点击主窗口(即不是一个按钮)。如果他们点击了在右手边的其中一个人,那么我们想选择这个人,并且转换到收集(Collection)模式。或者在收集(Collection)模式时,他们点击了主窗口,那么我们想转换到训练模式(Collection)。做法如下:

[cpp]  view plain copy
  1. else {  
  2. // 检测用户是否点击了列表中的脸  
  3. int clickedPerson = -1;  
  4. for (int i=0; i<m_numPersons; i++) {  
  5. if (m_gui_faces_top >= 0) {  
  6. Rect rcFace = Rect(m_gui_faces_left, m_gui_faces_top + i * faceHeight, faceWidth, faceHeight);  
  7. if (pt.inside(rcFace)) {  
  8. clickedPerson = i;  
  9. break;  
  10. }  
  11. }  
  12. }  
  13. // 如果用户点击了一个人脸,则改变选择的人  
  14. if (clickedPerson >= 0) {  
  15. // 转换当前人&收集更多的照片  
  16. m_selectedPerson = clickedPerson;  
  17. m_mode = MODE_COLLECT_FACES;  
  18. }  
  19. //反正他们点击了屏幕的中心,(进入训练模式)  
  20. else {  
  21. //如果正在收集人脸,改变到训练模式  
  22. if (m_mode == MODE_COLLECT_FACES) {  
  23. m_mode = MODE_TRAINING;  
  24. }  
  25. }  
  26. }  


总结


在这一章我们已经像你展示了创建一个实时人脸识别应用所需要的所有步骤,该步骤带有充足预处理,允许训练集环境和测试集环境的一下差异,仅采用基本的算法。我们使用人脸检测来找到相机图像中人脸的位置,接下来经过几种形式的人脸预处理来减少不同光照情况,相机和人脸方向以及面部表情的影响。然后我们用我们收集的预处理人脸来训练一个Eigenfaces或者Fisherfaces机器学习系统,并且最终我们进行人脸识别,通过提供一个信任度量来核实这个人是谁,以防它是一个未知的人(unknown person)


 

而不是提供一个命令行工具以离线的方式处理图像文件夹,我们组合上述所有的步骤到一个独立的实时GUI程序来允许快捷的使用人脸识别系统。你应当能够为你的自己的目的修改系统的行为,例如运行你计算机的自动登入,或者如果你对提高人脸识别的可靠型感兴趣,那么你可以阅读关于最先进人脸识别的文章来可能地改善程序的每一个步骤,直到满足你具体的需要。例如,你可以改善人脸预处理阶段,或者使用一个更先进的机器学习算法,或者甚至更好人脸验证算法,基于该网站的方法:

http://www.face-rec.org/algorithms/

http://www.cvpapers.com



参考文献


•  Rapid Object Detection using a Boosted Cascade of Simple Features, P. Viola 
and M.J. Jones, Proceedings of the IEEE Transactions on CVPR 2001, Vol. 1, 
pp. 511-518
•  An Extended Set of Haar-like Features for Rapid Object Detection, R. Lienhart and J. 
Maydt, Proceedings of the IEEE Transactions on ICIP 2002, Vol. 1, pp. 900-903
•  Face Description with Local Binary Patterns: Application to Face Recognition, T. 
Ahonen, A. Hadid and M. Pietikäinen, Proceedings of the IEEE Transactions on 
PAMI 2006, Vol. 28, Issue 12, pp. 2037-2041
•  Learning OpenCV: Computer Vision with the OpenCV Library, G. Bradski and A. 
Kaehler, pp. 186-190, O'Reilly Media.
•  Eigenfaces for recognition, M. Turk and A. Pentland, Journal of Cognitive 
Neuroscience 3, pp. 71-86
•  Eigenfaces vs. Fisherfaces: Recognition using class specific linear projection, P.N. 
Belhumeur, J. Hespanha and D. Kriegman, Proceedings of the IEEE Transactions on 
PAMI 1997, Vol. 19, Issue 7, pp. 711–720
•  Face Recognition with Local Binary Patterns, T. Ahonen, A. Hadid and M. 
Pietikäinen, Computer Vision - ECCV 2004, pp. 469–48

你可能感兴趣的:(LDA,pca)