在之前的博客中已经解决了人脸检测的问题,我们计划在这篇博客中介绍人脸识别、性别识别方面的相关实现方法。
其实性别识别和人脸识别本质上是相似的,因为这里只是一个简单的MFC开发,主要工作并不在算法研究上,因此我们直接将性别识别视为一种特殊的人脸识别模式。人脸识别可能需要分为几十甚至上百个类(因为有几十甚至上百个人),而性别识别则是一种特殊的人脸识别——只有两个类。
一、基本工具
通过OpenCv进行性别识别的基本工具是FaceRecognizer。这是OpenCv2.x版本中的一个基本的人脸识别类,它封装了三种基本但也是经典的人脸识别算法:基于PCA变换的人脸识别(EigenFaceRecognizer)、基于Fisher变换的人脸识别(FisherFaceRecognizer)、基于局部二值模式的人脸识别(LBPHFaceRecognizer)。这些算法差不多都是十年以前的人脸识别方法了,因此在今天看来正确率应该不会太让人满意,不过我们这里重在实践,而非算法研究(虽然本人就是搞图像识别算法研究的),因此我们不会在算法创新方面下太多功夫,所以选择了这三个基本的识别算法。
关于FaceRecognizer类人脸识别的详细操作,这里为大家推荐两篇博客:FaceRecognizer帮助文档以及FaceRecognizer。
这里我们直接使用FaceRecognizer类的相关操作方法,对于其基本用法就不再赘述。
二、数据集准备
进行性别识别理所应当需要先准备一些性别识别方面的训练样本,需要强调的一点是,数据集的准备过程中也需要一些小的技巧,在之后我会专门写一篇博文来解释如何制作一个简易的性别识别训练集,这里我们直接用我已经做好的训练集,下载地址:性别识别数据集
1、概况
我这里整理的性别识别训练集是取自中科院的人脸数据库CAS-PEAL的光照子集,包含400张男性人脸图片和400张女性人脸图片,剩余人脸图片作为测试样本
2、训练集基本结构
训练集包含三部分:男性样本、女性样本、测试样本:
这里我们通过CSV文件方法来批量读取训练样本,因此这里提前制作了一个txt文件来存储每一个训练样本图片的路径:
注意这里at.txt文件中的路径实际上是由两部分内容组成,即“路径;性别标号”。性别标号“1”代表男性,“2”代表女性。至于如何通过csv文件方法来批量读取文件,参见:一种批量读取文件的方法—CSV文件。
同理,在测试样本中同样需要用txt文件来记录样本路径和标签:
三、识别算法的训练与测试
1、新建一个控制台工程,配置OpenCv
这里不再赘述,建议加上预编译头即可,这里工程名暂定为GenderRecognition
2、编写批量读取文件函数read_csv()
首先,批量txt文件是典型的io操作,需要包含以下头文件:
#include#include #include
然后开始编写read_csv函数,函数相对比较简单,这里直接给出代码:
void read_csv(string& fileName,vector& images,vector<int>& labels,char separator = ';') { ifstream file(fileName.c_str(),ifstream::in); //以读入的方式打开文件 String line,path,label; while (getline(file,line)) //从文本文件中读取一行字符,未指定限定符默认限定符为“/n” { stringstream lines(line); getline(lines,path,separator); //根据指定分割符进行分割,分为“路径+标号” getline(lines,label); if (!path.empty()&&!label.empty()) //如果读取成功,则将图片和对应标签压入对应容器中 { images.push_back(imread(path,1)); //读取训练样本 labels.push_back(atoi(label.c_str())); //读取训练样本标号 } } }
read_csv()函数的主要功能就是读取指定目录下的路径文件(例如这里的at.txt),然后根据路径文件中的记录,逐行读入对应路径的训练样本路径及其标号,并放入对应容器(vector)中。至于为什么采用vector数据结构来存储训练样本,一是因为这样做简单直观,二是因为OpenCv的训练函数提供的是vector接口。当然这样做也存在一定弊端,就是必须一次性将训练样本全部读入到内存中,当训练样本数量庞大时这种方法不但会消耗掉巨额内存,而且效率低下。
更多关于read_csv()批量读取的知识参见一种批量读取文件的方法—CSV文件。
3、读入训练样本
接下来在主函数中调用read_csv()函数,读取训练样本及标签,并放入对应容器中:
int _tmain(int argc, _TCHAR* argv[]) { String csvPath = "E:\\性别识别数据库—CAS-PEAL\\at.txt"; vectorimages; vector<int> labels; read_csv(csvPath,images,labels); return 0; }
读取成功,images和labels两个容器都包含800个样本:
4、训练分类器
OpenCv中的FaceRecognizer类提供的分类器训练API函数非常简单,只需三句话,以EigenFaceRecognizer为例:
PtrmodelPCA = createEigenFaceRecognizer(); modelPCA->train(images,labels); modelPCA->save("E:\\性别识别数据库—CAS-PEAL\\PCA_Model.xml");
训练完成后(大约五分钟左右),训练好的分类器已经以XML文件的形式保存在了指定路径下:
同理,训练FisherFaceRecognizer、LBPHFaceRecognizer两个分类器并保存:
PtrmodelFisher = createFisherFaceRecognizer(); modelFisher->train(images,labels); modelFisher->save("E:\\性别识别数据库—CAS-PEAL\\Fisher_Model.xml"); Ptr modelLBP = createLBPHFaceRecognizer(); modelLBP->train(images,labels); modelLBP->save("E:\\性别识别数据库—CAS-PEAL\\LBP_Model.xml");
得到另外两个分类器:
4、测试分类器
训练完分类器后,接下来我们介绍如何使用这些训练好的分类器对测试样本进行分类。首先加载三个分类器
PtrmodelPCA = createEigenFaceRecognizer(); Ptr modelFisher = createFisherFaceRecognizer(); Ptr modelLBP = createLBPHFaceRecognizer(); modelPCA->load("E:\\性别识别数据库—CAS-PEAL\\PCA_Model.xml"); modelFisher->load("E:\\性别识别数据库—CAS-PEAL\\Fisher_Model.xml"); modelLBP->load("E:\\性别识别数据库—CAS-PEAL\\LBP_Model.xml");
然后读入一张测试样本,通过三个分类器对其进行预测:
Mat testImage = imread("E:\\性别识别数据库—CAS-PEAL\\测试样本\\男性测试样本\\face_480.bmp",0); int predictPCA = modelPCA->predict(testImage); int predictLBP = modelLBP->predict(testImage); int predictFisher = modelFisher->predict(testImage);
预测结果如图:
可见对于这张测试图片,三个分类器均给出了正确预测(数字“1”代表男性),正确率可以接受。
四、代码
这部分博客所涉及的代码同样较为简洁,因此在这里给出整体代码:
// GenderRecognition.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include#include #include #include using namespace std; using namespace cv; void read_csv(string& fileName,vector & images,vector<int>& labels,char separator = ';') { ifstream file(fileName.c_str(),ifstream::in); //以读入的方式打开文件 String line,path,label; while (getline(file,line)) //从文本文件中读取一行字符,未指定限定符默认限定符为“/n” { stringstream lines(line); getline(lines,path,separator); //根据指定分割符进行分割,分为“路径+标号” getline(lines,label); if (!path.empty()&&!label.empty()) //如果读取成功,则将图片和对应标签压入对应容器中 { images.push_back(imread(path,0)); //读取训练样本 labels.push_back(atoi(label.c_str())); //读取训练样本标号 } } } int _tmain(int argc, _TCHAR* argv[]) { String csvPath = "E:\\性别识别数据库—CAS-PEAL\\at.txt"; vector images; vector<int> labels; read_csv(csvPath,images,labels); Ptr modelPCA = createEigenFaceRecognizer(); modelPCA->train(images,labels); modelPCA->save("E:\\性别识别数据库—CAS-PEAL\\PCA_Model.xml"); Ptr modelFisher = createFisherFaceRecognizer(); modelFisher->train(images,labels); modelFisher->save("E:\\性别识别数据库—CAS-PEAL\\Fisher_Model.xml"); Ptr modelLBP = createLBPHFaceRecognizer(); modelLBP->train(images,labels); modelLBP->save("E:\\性别识别数据库—CAS-PEAL\\LBP_Model.xml"); //Ptr modelPCA = createEigenFaceRecognizer(); //PtrmodelFisher = createFisherFaceRecognizer(); //PtrmodelLBP = createLBPHFaceRecognizer(); modelPCA->load("E:\\性别识别数据库—CAS-PEAL\\PCA_Model.xml"); modelFisher->load("E:\\性别识别数据库—CAS-PEAL\\Fisher_Model.xml"); modelLBP->load("E:\\性别识别数据库—CAS-PEAL\\LBP_Model.xml"); Mat testImage = imread("E:\\性别识别数据库—CAS-PEAL\\测试样本\\男性测试样本\\face_480.bmp",0); int predictPCA = modelPCA->predict(testImage); int predictLBP = modelLBP->predict(testImage); int predictFisher = modelFisher->predict(testImage); return 0; }
四、总结
这篇博客主要介绍了如何使用OpenCv提供的人脸识别类FaceRecognizer来进行性别识别,并提供了一段win32控制台工程下的简洁代码,同时,有以下几个方面需要特别注意一下。
1、人脸识别和性别识别的关系
在这篇博客的开始部分曾提到过性别识别和人脸识别的关系,在这里需要再次强调一下。性别识别本质上属于人脸识别,但是和人脸识别还是有很多方面的区别。性别识别是二分类问题,人脸识别是多分类问题,二者在算法上也有很大差异。我们这里之所以简单的将性别识别看做简化的人脸识别,是因为在这套教程中我们主要注重实践,注重OpenCv的使用以及MFC框架编程方法,因此在算法方面会显得不够严谨。因此希望大家不要被这些简化的观点所误导,真正的性别识别算法也远比这些复杂,也和人脸识别方法大不相同,作为图像处理的行内人,我觉得很有必要把这点说清楚。
2、read_csv函数
这里对read_csv()批量读取函数介绍得相对简洁,大家可以参照我提供的博客来进行详细学习,同时考虑到这个函数相对简洁,可以凡在main()函数之前,从而避免提前声明。
3、数据集原始路径问题
这篇博文中并没有详细介绍如何制作性别识别训练数据集,因此大家在使用网上下载的数据集时一定要注意路径的问题。下载后数据集必须放在E盘根目录下,否则的话则需要重新制作路径文件(at.txt),不过这一步也并不复杂,参见一种批量读取文件的方法—CSV文件。
同时,这里在向路径文件后边添加类别标号时,当初我采用的是手动添加的方式,不过我相信大家能够找到更为简便的添加方式。
这里之所以没有介绍数据集的制作,是因为我计划将这部分内容作为程序的一个附加功能来单独进行介绍(也就是所谓的“人脸批量分割”),在之后进入到MFC编程部分时会进行专门的介绍。
4、关于性别识别的其他方法
在接下来的博文中我会介绍性别识别中的另外一种基础方法——SVM方法。