在人脸识别之一,已经做好人脸库了。
在这一篇,进行人脸识别模型的训练。
一、数据准备即生成csv文件
有了人脸库数据,我们需要在程序中读取它,这里需借助csv文件去读取人脸库中的图像数据。
一个csv文件格式:图片路径名+标签,如/path/to/image.jpg;1
假设人脸图像路径:/path/to/image.jpg
我们给这个人脸图像一个标签“1”,这个标签代表这个人的名字,同一个人的人脸图像标签须相同。
你可以手动创建文件并一个一个去敲:
/mnt/hgfs/share_ubuntu/BS-Project/creat_csv/faces/s1/1.jpg;1 /mnt/hgfs/share_ubuntu/BS-Project/creat_csv/faces/s1/2.jpg;1 /mnt/hgfs/share_ubuntu/BS-Project/creat_csv/faces/s1/3.jpg;1 /mnt/hgfs/share_ubuntu/BS-Project/creat_csv/faces/s1/4.jpg;1 /mnt/hgfs/share_ubuntu/BS-Project/creat_csv/faces/s1/5.jpg;1 /mnt/hgfs/share_ubuntu/BS-Project/creat_csv/faces/s1/6.jpg;1 /mnt/hgfs/share_ubuntu/BS-Project/creat_csv/faces/s1/7.jpg;1 /mnt/hgfs/share_ubuntu/BS-Project/creat_csv/faces/s1/8.jpg;1 /mnt/hgfs/share_ubuntu/BS-Project/creat_csv/faces/s1/9.jpg;1 /mnt/hgfs/share_ubuntu/BS-Project/creat_csv/faces/s1/10.jpg;1 /mnt/hgfs/share_ubuntu/BS-Project/creat_csv/faces/s2/1.jpg;2 /mnt/hgfs/share_ubuntu/BS-Project/creat_csv/faces/s2/2.jpg;2 /mnt/hgfs/share_ubuntu/BS-Project/creat_csv/faces/s2/3.jpg;2 ...
为了方便,我写了个 Linux-C 版本的创建csv文件的小程序,
代码:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <dirent.h> #include <fcntl.h> #include <unistd.h> int Create_CSV(char *dir_path); int main(int argc,char* argv[]) { if(argc != 2) { printf("usage: %s <path>\n", argv[0]); return -1; } Create_CSV(argv[1]); return 0; } /* 创建csv文件,参数 dir_path:目录路径(绝对路径) 仅针对两级目录,如: picture/s1,picture为第1级,s1为第2级 应在s1下放置图片文件 备注: 为方便起见,2级目录命名规则: 字符标识1位 + 数字编号(唯一) 如 s1 s2 x3 a4 d5 ... 数字编号代表特定的个体,不可重复 当然亦可取其他,标签会从文件夹名的第2字符开始取至末尾 */ int Create_CSV(char *dir_path) { struct stat statbuf; DIR *dir; DIR *dirFile; struct dirent *dirp; struct dirent *direntFile; char dir_path2[64] = {0}; char fileName[128] = {0}; char witeBuf[128] = {0}; int fd; if(dir_path[strlen(dir_path)-1] == '/') // 统一输入不以'/'结束,如 "/mnt/" 改为 "/mnt" dir_path[strlen(dir_path)-1] = 0; // 获取文件属性 if(lstat(dir_path, &statbuf) < 0) { printf("lstat(%s) failed !\n", dir_path); return -1; } // 判断是否为目录 if(S_ISDIR(statbuf.st_mode) != 1) { printf("%s is not dir !\n", dir_path); return -1; } // 打开目录 dir = opendir(dir_path); if( dir ==NULL) { printf("opendir failed.\n"); return -1; } // 创建或打开csv文件 fd = open("orl_faces.csv", O_RDWR | O_CREAT, 0777); if(fd < 0) { printf("open file faile.\n"); return -1; } // 定位读写位置 lseek(fd, 0, SEEK_SET); // 遍历一级目录 while((dirp = readdir(dir)) != NULL) { // 忽略 '.' '..'文件(linux) if( strncmp(dirp->d_name, ".", strlen(dirp->d_name))==0 || strncmp(dirp->d_name, "..", strlen(dirp->d_name))==0 ) continue; // 将1级目录与2级目录组合 memset(dir_path2, 0, sizeof(dir_path2)); strcat(dir_path2, dir_path); strcat(dir_path2, "/"); strcat(dir_path2, dirp->d_name); if(lstat(dir_path2, &statbuf) < 0) { printf("lstat(%s) failed !\n", dir_path2); continue; } if(S_ISDIR(statbuf.st_mode) != 1) { printf("%s is not dir !\n", dir_path2); continue; } dirFile = opendir(dir_path2); if( dirFile ==NULL) { printf("opendir failed.\n"); return -1; } // 遍历二级目录 while((direntFile = readdir(dirFile)) != NULL) { if( strncmp(direntFile->d_name, ".", strlen(direntFile->d_name))==0 || strncmp(direntFile->d_name, "..", strlen(direntFile->d_name))==0 ) continue; // 获取完整文件路径名 memset(fileName, 0, sizeof(fileName)); strcat(fileName, dir_path2); strcat(fileName, "/"); strcat(fileName, direntFile->d_name); if(lstat(fileName, &statbuf) < 0) { printf("lstat(%s) failed !\n", fileName); continue; } if(S_ISREG(statbuf.st_mode) != 1) // 不是普通文件 { printf("%s is not reg file !\n", fileName); continue; } memset(witeBuf, 0, sizeof(witeBuf)); memcpy(witeBuf, fileName, strlen(fileName)); strcat(witeBuf, ";"); strcat(witeBuf, dirp->d_name+1); // 标签: 从文件夹名的第2个字符开始 strcat(witeBuf, "\n"); printf("%s", witeBuf); // 写入信息 write(fd, witeBuf, strlen(witeBuf)); } } closedir(dirFile); closedir(dir); close(fd); return 0; }
执行参数:人脸库的一级目录路径(绝对路径),如:./a.out /mnt/hgfs/share_ubuntu/BS-Project/creat_csv/orl_faces
执行结果: 可见当前目录下多了一个 faces.csv 文件,其内容如下(未显示完整):
至此,创建 csv 文件成功!
二、人脸识别模型训练
首先,官方示例:
https://docs.opencv.org/3.2.0/da/d60/tutorial_face_main.html
官方给出三个方法:特征脸EigenFace、LDA线性差别分析Fisherface、局部二值模式直方图LBPH
能力有限,暂不详解。 结合三个示例,综合成一个含有三种方法的程序:
#include "opencv2/core.hpp" #include "opencv2/face.hpp" #include "opencv2/highgui.hpp" #include "opencv2/imgproc.hpp" #include <iostream> #include <fstream> #include <sstream> using namespace cv; using namespace cv::face; using namespace std; static Mat norm_0_255(InputArray _src) { Mat src = _src.getMat(); // 创建和返回一个归一化后的图像矩阵: Mat dst; switch(src.channels()) { case 1: cv::normalize(_src, dst, 0, 255, NORM_MINMAX, CV_8UC1); break; case 3: cv::normalize(_src, dst, 0, 255, NORM_MINMAX, CV_8UC3); break; default: src.copyTo(dst); break; } return dst; } //使用CSV文件去读图像和标签,主要使用stringstream和getline方法 static void read_csv(const string& filename, vector<Mat>& images, vector<int>& labels, char separator = ';') { std::ifstream file(filename.c_str(), ifstream::in); if (!file) { string error_message = "No valid input file was given, please check the given filename."; CV_Error(Error::StsBadArg, error_message); } string line, path, classlabel; while (getline(file, line)) { stringstream liness(line); getline(liness, path, separator); getline(liness, classlabel); if(!path.empty() && !classlabel.empty()) { images.push_back(imread(path, 0)); labels.push_back(atoi(classlabel.c_str())); } } } int main(int argc, const char *argv[]) { if(argc != 2) { printf("usage: %s <csv_file>\n", argv[0]); return -1; } //读取你的CSV文件路径. string fn_csv = string(argv[1]); // string fn_csv = "at.csv"; // 2个容器来存放图像数据和对应的标签 vector<Mat> images; vector<int> labels; // 读取数据. 如果文件不合法就会出错 // 输入的文件名已经有了. try { read_csv(fn_csv, images, labels); } catch (cv::Exception& e) { cerr << "Error opening file \"" << fn_csv << "\". Reason: " << e.msg << endl; // 文件有问题,我们啥也做不了了,退出了 exit(1); } // 如果没有读取到足够图片,也退出. if (images.size() <= 1) { string error_message = "This demo needs at least 2 images to work. Please add more images to your data set!"; CV_Error(CV_StsError, error_message); } // 下面的几行代码仅仅是从你的数据集中移除最后一张图片 //[gm:自然这里需要根据自己的需要修改,他这里简化了很多问题] Mat testSample = images[images.size() - 1]; int testLabel = labels[labels.size() - 1]; images.pop_back(); labels.pop_back(); // 下面几行创建了一个特征脸模型用于人脸识别, // 通过CSV文件读取的图像和标签训练它。 // T这里是一个完整的PCA变换 //如果你只想保留10个主成分,使用如下代码 // cv::createEigenFaceRecognizer(10); // cv::createFisherFaceRecognizer(10); // // 如果你还希望使用置信度阈值来初始化,使用以下语句: // cv::createEigenFaceRecognizer(10, 123.0); // // 如果你使用所有特征并且使用一个阈值,使用以下语句: // cv::createEigenFaceRecognizer(0, 123.0); // cv::createFisherFaceRecognizer(0, 123.0); Ptr<BasicFaceRecognizer> model0 = createEigenFaceRecognizer(); model0->train(images, labels); model0->save("MyFacePCAModel.xml"); Ptr<BasicFaceRecognizer> model1 = createFisherFaceRecognizer(); model1->train(images, labels); model1->save("MyFaceFisherModel.xml"); Ptr<LBPHFaceRecognizer> model2 = createLBPHFaceRecognizer(); model2->train(images, labels); model2->save("MyFaceLBPHModel.xml"); // 下面对测试图像进行预测,predictedLabel是预测标签结果 int predictedLabel0 = model0->predict(testSample); int predictedLabel1 = model1->predict(testSample); int predictedLabel2 = model2->predict(testSample); // 还有一种调用方式,可以获取结果同时得到阈值: // int predictedLabel = -1; // double confidence = 0.0; // model->predict(testSample, predictedLabel, confidence); string result_message0 = format("Predicted class = %d / Actual class = %d.", predictedLabel0, testLabel); string result_message1 = format("Predicted class = %d / Actual class = %d.", predictedLabel1, testLabel); string result_message2 = format("Predicted class = %d / Actual class = %d.", predictedLabel2, testLabel); cout << result_message0 << endl; cout << result_message1 << endl; cout << result_message2 << endl; waitKey(0); return 0; }
编译、运行:(备注:faces.csv是第一步得到的csv文件)
$ cmake . $ make $ ./train faces.csv
输出: 可看到,检测最后一张人脸图,识别的都准确。
root@qihua-virtual-machine:/mnt/hgfs/Project/opencvPro/train# ./train faces.csv Predicted class = 4 / Actual class = 4. Predicted class = 4 / Actual class = 4. Predicted class = 4 / Actual class = 4.
同时,可见到当前目录下多了3个文件:
MyFaceFisherModel.xml、MyFaceLBPHModel.xml、MyFacePCAModel.xml
这几个就是用不同方法训练的人脸模型了。
留着后续用到。
人脸模型训练至此!