关于使用OpenCV的LBPHFaceRecognizer实现人脸的采集、训练与更新、应用的C++实现DEMO
作者:Simon Song
在实际人脸识别中,有很多可用方法,如OpenCV自带的EigenFaceRecognizer(基于PCA降维),FisherFaceRecognizer(基于LDA降维),LBPHFaceRecognizer(基于LBPH特征),其中只有LBPHFaceRecognizer是支持直接更新的模型算法;再如faceNet深度网络模型(128个特征输出)加分类器(如SVM)方式。其他商业应用如旷视科技Megvii Face++,百度AI,等等很多。
我们需要应用的场景是结果好,对于硬件要求不是很高,方便更新模型,低成本的方式。因此我选择了OpenCV自带的LBPHFaceRecognizer算法,这种算法优点是不会受到光照、缩放、旋转和平移的影响,这钟算法最大的好处就是可以在性能不是很高的ARM板上也可执行,通用化非常好。
模块分为三个部分:
1.采集图片部分:采集部分用于采集人脸图片,对应采集好的图片需要人工挑选和整理,并形成文件清单文件(如filelist.txt);对于更新部分,制作updatelist.txt文件;制作分类号与姓名的字典对应关系(dict.txt)。
此处的文件清单每行格式为:文件路径,分类号
此处的字典文件每行格式为:分类号:姓名
2.训练与更新部分:使用整理好的训练集合文件清单进行训练,生成xml模型文件;更新只是在原有模型基础上增加分类;对模型进行图片准确率测试。
3.应用部分:读入训练好的模型,对人脸进行识别。
vs2015,OpenCV341,C++
#include
#include
#include
#include //创建目录等
#include //文件是否存在
using namespace std;
using namespace cv;
/*
此文件用于采集人脸数据,人工点击采集的图片,再有人工检测可用性即可。
*/
string cascade_file = "G:/opencv_trainning/vs2015/myself_project/FaceRccognizer_LBPH/face_collection/haarcascade_frontalface_alt.xml";//定义级联文件路径
string save_path = "G:/opencv_trainning/vs2015/myself_project/FaceRccognizer_LBPH/sample/%d/";//保存图片路径
int main(int argc,char** argv) {
//定义摄像头设备
VideoCapture capture(2);
//调整摄像头分辨率
//capture.set(CV_CAP_PROP_FRAME_WIDTH,1024);//设置宽
//capture.set(CV_CAP_PROP_FRAME_HEIGHT,768);//设置高
if (!capture.isOpened()) {//未打开,报错
cout << "capture open fault!" << endl;
return -1;
}
//定义必要的变量
CascadeClassifier cascade_face = CascadeClassifier(cascade_file);//定义级联变量并读取级联文件
vector<Rect> faces;//定义人脸位置矩形向量
Mat frame;//定义帧图,用于U读取
int count = 0;//定义统计数量
long current_tick_count = 0;//记录子目录名的数字
bool save_state = false;//保存状态,用于用于控制
//循环读取帧图
while (capture.read(frame)){
//printf("width=%d,height=%d\n",frame.cols,frame.rows);//打印提示
//左右翻转,参数:(原图,目标图,反转代码)
flip(frame,frame,1);
//级联发现脸,参数:(图,矩形向量,尺度系数,高斯金字塔最小临近值,标记(0),最小尺寸,最大尺寸)
cascade_face.detectMultiScale(frame,faces,1.09,1,0,Size(200,200),Size(400,400));
//保存脸和绘制脸位置
for (int i = 0; i < faces.size();i++) {//循环脸向量位置
//获得人脸部分矩形
Rect rect = faces[i];
rect.x = rect.x + rect.width*0.16;//重定x
rect.y = rect.y + rect.height*0.10;//重定y
rect.width = rect.width*0.70;//重定宽
rect.height = rect.height*0.78;//重定高
//当为保存状态,且为10的倍数,且为小于401时,保存图片(保存40张图)
if (save_state&&count%10==0&&count<401){
Mat dst;//定义新图
//调整脸图片大小,128*128,参数:(原图,目标图,尺寸,x比例,y比例,线性插值类型)
resize(frame(rect),dst,Size(128,128),0,0,INTER_LINEAR);
//保存到指定位置
string path = format(save_path.c_str(), current_tick_count);//获得完整路径,以当前时间数的决定值为子目录名
//判断路径是否存在
/*
R_OK 只判断是否有读权限
W_OK 只判断是否有写权限
X_OK 判断是否有执行权限
F_OK 只判断是否存在
在宏定义里面分别对应:
0x00 只存在
0x02 写权限
0x04 读权限
0x06 读和写权限
*/
if (_access(path.c_str(), _A_NORMAL)==-1) {//当路径不存在(-1),创建路径
_mkdir(path.c_str());//创建路径
}
path = path + string(format("%d.jpg",count/10));//添加路径文件名
imwrite(path.c_str(),dst);//保存图片,参数:(路径名,矩阵图)
//提示正在保存图片,头不要动,以眼睛作为基准线,变换表情。
string content("Don't move your head. Use your eyes as a baseline to change your expression.");//指定文本
//添加文本到图片,参数:(图,文本,原点,字体,缩放比,颜色,线宽,线型,左底起源状态(倒置效果))
putText(frame,content, Point(20, 30), CV_FONT_NORMAL, 0.5, Scalar(0,255, 0), 1, 8, false);
}
else if (save_state&& count<401) {//当保存状态为真,且统计数小于401时,说明在记录范围内,只提示
//提示正在保存图片,头不要动,以眼睛作为基准线,变换表情。
string content("Don't move your head. Use your eyes as a baseline to change your expression.");//指定文本
//添加文本到图片,参数:(图,文本,原点,字体,缩放比,颜色,线宽,线型,左底起源状态(倒置效果))
putText(frame, content, Point(20, 30), CV_FONT_NORMAL, 0.5, Scalar(0, 255, 0), 1, 8, false);
}else {//否则提示等待状态
//添加文本到图片,参数:(图,文本,原点,字体,缩放比,颜色,线宽,线型,左底起源状态(倒置效果))
putText(frame,"Wait for the key to be pressed.",Point(20,30),CV_FONT_NORMAL,1.0,Scalar(255,0,0),1,8,false);
}
//绘制矩形,参数:(图,矩形变量,颜色,线宽,线型,偏移量)
rectangle(frame, rect, Scalar(0, 255, 0), 1, 8, 0);
}
//显示图
imshow("frame",frame);
//按键判断
char c = waitKey(33);//等待33毫秒并获得按键值,-1时表示没有输入
if (c==27) {//当为ESC(27)时,退出循环
break;
}else if(c!=-1) {//当用户点击除ESC的任意键时,应拍照保存人脸部分或改变状态
save_state = (save_state ? false : true);//反转保存状态,当保存状态为真时,给假,否则给真
count = 0;//统计数量给0,从新计数
current_tick_count = (save_state?abs(getTickCount()):0);//当保存状态为真是,获得当前时间统计值的绝对值,否则为0
}
count++;//统计变量加1
}
//释放摄像头资源
capture.release();
return 0;
}
#include
#include
#include
#include
#include //文件是否存在
using namespace std;
using namespace cv;
using namespace cv::face;//脸模块
/*
此文件用于训练LBPHFaceRecognizer模型。
*/
//定义路径
string train_image_path = "G:/opencv_trainning/vs2015/myself_project/FaceRccognizer_LBPH/sample/filelist.txt";//训练集清单文件
string update_image_path = "G:/opencv_trainning/vs2015/myself_project/FaceRccognizer_LBPH/sample/updatelist.txt";//更新集清单文件
string face_model = "G:/opencv_trainning/vs2015/myself_project/FaceRccognizer_LBPH/sample/face_model.xml";//face分类模型文件
string test_image_path = "G:/opencv_trainning/vs2015/myself_project/FaceRccognizer_LBPH/testimage/testlist.txt";//测试图路径
//声明函数
bool getMatAndLabels(string& filelist_txt,vector<Mat>& mats,vector<int>&labels);//获得图片矩阵和标签
bool train_lbphface(vector<Mat>& mats, vector<int>&labels);//模型训练
bool update_lbphface(vector<Mat>& mats, vector<int>&labels);//更新训练模型
vector<int> lbphface_predict(vector<Mat>& testImages);//lbphface预测函数
int main(int argc,char** argv) {
//1.读取标签文件及对应的人脸图片
vector<Mat> train_mats;//定义矩阵向量
vector<int> train_labels;//定义标签向量
int64 time_rec=getTickCount();//记录当前时间,用于统计训练时间
bool result = false;//定义结果变量
//2.判断操作
//2.1当没有模型时,读取训练数据,否则不读取训练数据
if(_access(face_model.c_str(),_A_NORMAL)==-1){//模型不存在的处理
//2.1.1获取矩阵和标签(自定义),参数:(文件路径,训练矩阵向量,训练标签向量)
result=getMatAndLabels(train_image_path,train_mats,train_labels);
if (!result) {//当结果为假时
cout << "getMatAndLabels is fault!" << endl;
return -1;//直接返回
}
//2.1.2训练模型(自定义)
result = train_lbphface(train_mats, train_labels);//参数:(矩阵向量,标签向量)
if (!result) {//当结果为假时
cout << "train_face is fault!" << endl;
return -1;//直接返回
}
}
//2.2检查是否有更新文件列表,有则读入,无则无操作
if (_access(update_image_path.c_str(), _A_NORMAL) == 0) {//当有更新文件时,参数:(文件路径指针,检查标记)
//2.2.1获取矩阵和标签(自定义),参数:(文件路径,训练矩阵向量,训练标签向量)
result = getMatAndLabels(update_image_path, train_mats, train_labels);
if (!result) {//当结果为假时
cout << "getMatAndLabels is fault!" << endl;
return -1;//直接返回
}
//2.2.2更新训练模型(自定义)
result = update_lbphface(train_mats, train_labels);//参数:(矩阵向量,标签向量)
if (!result) {//当结果为假时
cout << "update_face is fault!" << endl;
return -1;//直接返回
}
}
cout << "total_train_time(s)="<<(getTickCount()-time_rec)/getTickFrequency()<<"s"<< endl;//计算训练时间并打印
time_rec = getTickCount();//记录当前时间,用于统计总预测时间
//3.给定图片测试结果
//读取标签文件及对应人脸
vector<Mat> test_mats;//测试矩阵
vector<int> test_labels;//测试标签
vector<int> pre_labels;//预测标签
//获取矩阵和标签(自定义),参数:(文件路径,训练矩阵向量,训练标签向量)
result=getMatAndLabels(test_image_path,test_mats,test_labels);
if (!result) {//当结果为假时
cout << "test:getMatAndLabels is fault!" << endl;
return -1;//直接返回
}
//预测处理(自定义),参数:(测试图向量),返回预测结果向量
pre_labels = lbphface_predict(test_mats);
if (pre_labels.empty()) {//当返回向量为空
cout << "pre_lables is empty!" << endl;
return -1;//返回-1
}
//计算准确率
Mat pre_mat = Mat::zeros(test_labels.size(),1,CV_8UC1);//定义一个预测矩阵,N行1列,1通道
for (int i = 0; i < test_mats.size();i++) {//循环矩阵位置
//比较是否相等
if (test_labels[i]==pre_labels[i]) {//当相等时
pre_mat.at<uchar>(i)=1;//预测矩阵位置给1
}
//printf("test_labels[%d]=%d,pre_labels[%d]=%g\n",i,test_labels[i],i,pre_labels[i]);//打印提示
}
//cout << "pre_mat=" << pre_mat <<","<
Scalar tempVal=mean(pre_mat);//获得均值数据
float matMean = tempVal.val[0];//获得数据中的第一个位置的值作为矩阵均值(单通道图),此均值为准确率
cout << "total_predict_time(s)=" << (getTickCount() - time_rec) /getTickFrequency() <<"s"<< endl;//计算总预测时间并打印
printf("Mat Mean(accuracy) is %.2f\n", matMean);//打印提示
imshow("pre_mat", pre_mat);//为了时waitKey()有效等待,此处有个显示
waitKey(0);//等待用户按键退出程序
return 0;
}
bool getMatAndLabels(string& filelist_txt, vector<Mat>& mats, vector<int>&labels) {//获得图片矩阵和标签
//定义必要的变量,参数:(文件名字符串,模式(in/out)),其他参数默认
ifstream file(filelist_txt,ifstream::in);
if (!file.is_open()) {//当不是被打开
cout << "file is not open!" << endl;
return false;//返回false
}
//读取文件每一行
string lines;//定义行字符串变量
while (!file.eof()) {//当不是EOF时,继续读取
//读取一行,参数:(文件流,字符串变量)
getline(file,lines);
//定义字符串流
stringstream line_stream(lines);
//读取图片路径
string path;//定义路径变量
getline(line_stream,path,',');//读取路径,用逗号分隔符为结束
//读取标签
string label;//定义标签变量
getline(line_stream,label);//读取其余部分,标签部分
//读取路径指定的图片(彩色)
Mat img = imread(path,IMREAD_GRAYSCALE);//读取灰度图
//调整图片为128*128,参数:(原图矩阵,目标图矩阵,图尺寸,x比例,y比例,插值类型)
resize(img,img,Size(128,128),0,0,INTER_LINEAR);
//存入向量
mats.push_back(img.clone());//存入矩阵向量
labels.push_back(atoi(label.c_str()));//存入标签向量
}
//判读向量状态
if (mats.empty()||labels.empty()||mats.size()!=labels.size()) {//当向量为空,或两向量个数不相等时,返回假
return false;//返回false
}
return true;//正常返回真
}
bool train_lbphface(vector<Mat>& mats, vector<int>&labels) {//模型训练
//判断参数
if (mats.empty()||labels.empty()||mats.size()!=labels.size()) {//当向量为空,或尺寸不相等时,返回假
return false;//返回false
}
//读取模型
Ptr<LBPHFaceRecognizer> model = LBPHFaceRecognizer::create();
//训练模型
model->train(mats,labels);//参数:(矩阵向量,标签向量)
//保存模型
model->save(face_model);//参数:(文件名字符串)
return true;//返回真
}
bool update_lbphface(vector<Mat>& mats, vector<int>&labels) {//更新训练模型
//判断参数
if (mats.empty() || labels.empty() || mats.size() != labels.size()) {//当向量为空,或尺寸不相等时,返回假
return false;//返回false
}
//读取模型
Ptr<LBPHFaceRecognizer> model = Algorithm::load<LBPHFaceRecognizer>(face_model);//参数:(模型全路径)
//更新训练模型
model->update(mats, labels);//参数:(矩阵向量,标签向量)
//保存模型
model->save(face_model);//参数:(文件名字符串)
return true;//返回真
}
vector<int> lbphface_predict(vector<Mat>& testImages){//lbphface预测函数
//定义结果向量
vector<int> result;
//判断向量合法性
if (testImages.empty()) {//当向量为空时
return result;//返回空的结果向量
}
//读取svm模型
Ptr<LBPHFaceRecognizer> model = Algorithm::load<LBPHFaceRecognizer>(face_model);//参数:模型文件
if(model->empty()){//当模型为空
cout << "face model load fault!" << endl;
return result;//返回空的结果向量
}
//定义必要的变量
int label = -1;//标签变量
double confidence_distance = 0;//置信值
//循环遍历矩阵向量
for (int i = 0; i < testImages.size();i++) {
//预测分类结果,参数:(图矩阵,标签变量,置信概率变量)
model->predict(testImages[i],label, confidence_distance);
//cout << "confidence_value=" << confidence_distance << endl;//测试
//cout << "label=" << label << endl;//测试
if (confidence_distance>80) {//当置信距离大于80时,给标签-1表示未知
label = -1;//标签给-1
}
//存入向量
result.push_back(label);
}
return result;//返回向量
}
#include
#include
#include
#include
#include //文件是否存在
using namespace std;
using namespace cv;
using namespace cv::face;//脸模块
/*
此文件用于人脸识别应用
*/
//定义路径
string face_model = "G:/opencv_trainning/vs2015/myself_project/FaceRccognizer_LBPH/sample/face_model.xml";//LBPH分类模型文件
string dict_path = "G:/opencv_trainning/vs2015/myself_project/FaceRccognizer_LBPH/sample/dict.txt";//字典路径
string cascade_file = "G:/opencv_trainning/vs2015/myself_project/FaceRccognizer_LBPH/sample/haarcascade_frontalface_alt.xml";//定义级联文件路径
//声明函数
vector<string> getDict(string& dict_path);//获得字典,返回字符串向量
vector<int> faces_predict(Ptr<LBPHFaceRecognizer>& face_model, vector<Mat>& testImages);//LBPHFaceRecognizer预测函数
int main(int argc,char** argv) {
//判断级联文件是否存在
if (_access(cascade_file.c_str(),_A_NORMAL)==-1) {//不存在(-1),直接退出
cout << "cascade xml is not exist" << endl;
return -1;
}
//判断face模型文件是否存在
if (_access(face_model.c_str(),_A_NORMAL)==-1) {//不存在(-1),直接退出
cout << "face_model xml is not exist" << endl;
return -1;
}
//读入face模型
Ptr<LBPHFaceRecognizer> model = Algorithm::load<LBPHFaceRecognizer>(face_model);//参数:(模型文件)
if (model->empty()) {//当模型为空
cout << "face model load fault!" << endl;
return -1;//返回-1
}
//读取字典(自定义函数)
vector<string> dict;//定义字典向量
dict = getDict(dict_path);//参数:(字典路径)
if (dict.empty()) {//当字典为空时
cout << "dictionary file load fault!" << endl;
return -1;
}
//定义摄像头设备
VideoCapture capture(2);
//摄像头像素
capture.set(CAP_PROP_FRAME_WIDTH, 640);//宽1024
capture.set(CAP_PROP_FRAME_HEIGHT, 480);//高768
if (!capture.isOpened()) {//当设备为打开
cout << "capture open fault!" << endl;
return -1;//返回-1
}
//定义必要的变量
Mat frame;//帧图
CascadeClassifier cascade_face(cascade_file);//定义级联分类器
vector<Rect> faces;//定义脸向量
//制作一个掩码图,用于指定有效区域
capture.read(frame);//读取一帧获取图片尺寸
cout << "frame.size()=" << frame.size() << endl;
Mat mask=Mat::zeros(frame.size(),CV_8UC3);//定义掩码图,与帧图保持一致
//绘制一个矩形并填充,参数:(x,y,width,height)
Rect mask_rect(frame.cols/6,frame.rows/6,frame.cols*0.67,frame.rows*0.67);//定义矩形信息
rectangle(mask,mask_rect,Scalar(255,255,255),-1);//填充有效区域,参数:(图,矩形变量,颜色,线宽(-1填充),线型,偏移量),线型和偏移量默认
rectangle(mask, mask_rect, Scalar(255, 0, 255), 20);//有效区域边缘绘色,参数:(图,矩形变量,颜色,线宽(-1填充),线型,偏移量),线型和偏移量默认
//imshow("mask",mask);
namedWindow("frame",CV_WINDOW_NORMAL);//定义显示窗口
//setWindowProperty("frame",CV_WND_PROP_FULLSCREEN,CV_WINDOW_FULLSCREEN);//设置全屏显示
//循环读取帧
while (capture.read(frame)) {//可以读到时继续
//左右翻转,参数:(图,目标图,翻转代码),翻转代码1表示左右,0表示上下,-1对角换
flip(frame,frame,1);
//合并图,参数:(图1,占比,图2,占比,目标图,标准差,数据类型(-1不变))
addWeighted(frame,0.85,mask,0.15,0,frame,-1);
//级联发现脸,参数:(图,矩形向量,尺度系数,高斯金字塔最小临近值,标记(0),最小尺寸,最大尺寸)
cascade_face.detectMultiScale(frame(mask_rect), faces, 1.09, 1, 0, Size(200, 200), Size(400, 400));//此处在掩码区域识别脸
if (faces.empty()) {//当向量为空时
imshow("frame", frame);//显示
char c = waitKey(33);//延时等待33ms并获取输入,无输入为-1
if (c == 27) {//当输入为ESC(27)时,中断while
break;//中断while
}
continue;//继续下一次
}
//保存脸和绘制脸位置
vector<Mat> vec_faces;//定义脸向量,方便预测
for (int i = 0; i < faces.size(); i++) {//循环脸向量位置
//获得人脸部分矩形
Rect rect = faces[i];
rect.x = mask_rect.x+rect.x + rect.width*0.16;//重定x
rect.y = mask_rect.y+rect.y + rect.height*0.10;//重定y
rect.width = rect.width*0.70;//重定宽
rect.height =rect.height*0.78;//重定高
//发现脸并存入矩阵向量
Mat dst;//定义新图
//调整脸图片大小,128*128,参数:(原图,目标图,尺寸,x比例,y比例,线性插值类型)
resize(frame(rect), dst, Size(128, 128), 0, 0, INTER_LINEAR);
vec_faces.push_back(dst.clone());//添加到脸向量,需要clone()操作
//绘制识别脸位置矩形,参数:(图,矩形变量,颜色,线宽,线型,偏移量)
rectangle(frame,Rect(rect.x,rect.y,rect.width,rect.height),Scalar(255,0,0),1,8,0);
}
//预测结果,参数:(face模型,脸矩阵向量),返回int结果向量
vector<int> pre_result = faces_predict(model,vec_faces);
if (pre_result.empty()) {//当返回向量为空
continue;//继续下一次操作
}
//绘制结果到图中
for (int i = 0; i < faces.size(); i++) {//循环脸向量位置
Rect rect = faces[i];//获得当前脸矩形
//调整一下x和y
rect.x += mask_rect.x;//加入大尺寸位置
rect.y += mask_rect.y;//加入大尺寸位置
//根据预测结果添加文字到图片,参数:(图,文本,原点,字体,缩放比,颜色,线宽,线型,左底起源状态(倒置效果))
putText(frame,dict[static_cast<int>(pre_result[i])+1], Point(rect.x, rect.y), CV_FONT_NORMAL, 0.5, Scalar(0, 255, 0), 1, 8, false);
//绘制矩形,参数:(图,矩形变量,颜色,线宽,线型,偏移量)
rectangle(frame, rect, Scalar(255, 255, 0), 1, 8, 0);
}
//显示
imshow("frame",frame);
char c=waitKey(33);//延时等待33ms并获取输入,无输入为-1
if (c==27) {//当输入为ESC(27)时,中断while
break;//中断while
}
//后台或其他指令操作
}
return 0;
}
vector<string> getDict(string& dict_path) {//获得字典,返回字符串向量
//定义必要的变量
vector<string> result;//定义字符串结果向量
ifstream file(dict_path, ifstream::in);//定义文件流,参数:(文件名字符串,模式(in/out)),其他参数默认
if (!file.is_open()) {//当不是被打开
cout << "file is not open!" << endl;
return result;//返回false
}
//读取文件每一行
string lines;//定义行字符串变量
while (!file.eof()) {//当不是EOF时,继续读取
//读取一行,参数:(文件流,字符串变量)
getline(file, lines);
//定义字符串流
stringstream line_stream(lines);
//读取图片路径
string label;//定义标签变量
getline(line_stream, label, ':');//读取标签,用分号分隔符为结束
//读取标签
string name;//定义名字变量
getline(line_stream, name);//读取其余部分,名字部分
//存入向量
result.push_back(name);
}
return result;//返回结果向量
}
vector<int> faces_predict(Ptr<LBPHFaceRecognizer>& face_model, vector<Mat>& testImages){//LBPHFaceRecognizer预测函数
//定义结果向量
vector<int> result;
//判断向量合法性
if (testImages.empty()) {//当向量为空时
return result;//返回空的结果向量
}
//判断模型可用性
if (face_model->empty()) {//当模型为空
cout << "face model is fault!" << endl;
return result;//返回空的结果向量
}
//定义变量
int label = -1;//分类值
double confidence_distance = 0;//置信概率值
//循环遍历矩阵向量
for (int i = 0; i < testImages.size(); i++) {
Mat img(testImages[i]);//定义新图
//转化为灰度图
Mat gray;
cvtColor(img,gray,CV_BGR2GRAY);//参数:(原图,目标图,转换模式)
//预测分类结果,参数:(图矩阵,标签变量,置信概率)
face_model->predict(gray,label, confidence_distance);
//printf("label=%d,confidence_distance=%g,", label, confidence_distance);//打印测试
if (confidence_distance>80) {//当置信距离大于80时
label = -1;//标签为-1,表示未知
}
//存入向量
result.push_back(label);
}
//printf("\n");//打印测试
return result;//返回向量
}