利用openCV的PCA+SVM实现人脸识别,即用PCA对数据集进行降维,保留主要的成分,然后送至SVM进行训练,对于当训练数据特别庞大时,可以达到缩小SVM模型提升分类速度的目的。
欢迎一起学习交流!
软件开发平台:visual studio 2013
opencv version:3.3.0
请注意上面两个的版本要匹配才行,opencv3.3.0有VC12的lib库,可以在网上查一下visual studio和VC的对应版本,这里visual studio 2013对应的就是VC12
首先给大家看一下我的VS目录结构,DataBase中就是存放的数据集,其中的图像都是未经处理过的含有人脸的图片。
本文采用读取TXT文件的方式,依次读入所需图片数据:
PathForTrain.txt包含了训练集的路径:
PathForTest.txt包含了测试机的路径:
首先实现一个根据TXT文件路径读取所以图片的函数:
vector<vector<String> > getImgPath(string s,int& num){
ifstream path(s);
vector<string> ipath;//多少个文件夹
string buf;
while (path){
if (getline(path, buf)){
ipath.push_back(buf);//图像所在的文件夹
}
}
path.close();
vector<vector<String> > allpath;//所有文件夹下的图片(*.jpg)文件的路径
for (size_t i = 0; i < ipath.size(); i++){
string pattern_jpg = ipath[i];
vector<String> files;
cv::glob(pattern_jpg, files);
if (files.size() == 0) {
std::cout << "No image files[jpg]" << std::endl;
}
num += files.size();//每个文件夹下的图片数量之和
allpath.push_back(files);
}
return allpath;
}
然后调用这个函数,举个例子:
//1. 通过path.txt确定需要读取的图片文件夹和类别
vector<vector<String> > imgByDir;//按文件夹存储图片路径的向量,每个文件夹代表不同的label
int nImgNum = 0; //nImgNum是样本数量
imgByDir = getImgPath("PathForTrain.txt", nImgNum);
cout << "共有样本个数为:" << nImgNum <<
本文的目的是实现人脸识别,所以首先需要进行人脸检测,把人脸数据送去降维和分类,这里用opencv自带的人脸检测模型,这里分两步:
第一步加载人脸检测分类器
//0. 加载人脸检测模型
String face_cascade_name = "src/haarcascade_frontalface_alt.xml";
CascadeClassifier face_cascade;
if (!face_cascade.load(face_cascade_name)){ printf("--(!)Error loading\n"); return -1; };
第二步对人脸区域图像裁剪
Mat getFaceImg(Mat src, CascadeClassifier cascade){
//人脸检测
std::vector<Rect> faces;
Mat face = src;
//equalizeHist(src,src);//直方图均衡化
cascade.detectMultiScale(src, faces, 1.1, 2, 0 | CASCADE_SCALE_IMAGE, Size(30, 30));
//裁剪出人脸
for (size_t f = 0; f < faces.size(); f++){
if (faces[f].width > 90){
face = src(faces[f]);
break;
}
}
//统一图片大小
Mat resized;
resize(face, resized, Size(Width, Height));
normalize(resized, resized, 0, 255, NORM_MINMAX);//归一化
return resized;
}
//2. 加载文件夹下的图片并进行前期图像处理和特征提取
//data_mat为所有训练样本的特征向量组成的矩阵,行数等于所有样本的个数,列数等于训练图片的维数
Mat data_mat = Mat(nImgNum, Height*Width, CV_8UC1);
//labels_mat为训练样本的类别向量,行等于样本个数
Mat labels_mat = Mat(nImgNum, 1, CV_32SC1);
int index = 0;//data_mat、labels_mat的下标
for (unsigned int i = 0; i < imgByDir.size(); i++){//遍历每个文件夹下的jpg文件,imgByDir.size()就是样本的类别,一个文件夹下放相同标签的照片
for (size_t j = 0; j < imgByDir[i].size(); j++){
//读取灰度图片
Mat image0 = cv::imread(imgByDir[i][j], 0);
if (image0.empty()){
cout << " can not load the image: " << imgByDir[i][j].c_str() << endl;
continue;
}
Mat faceImg = getFaceImg(image0, face_cascade);
//imshow("face", faceImg);
//waitKey(1);
Mat reshaped = Mat(1, Height*Width, CV_32SC1);
reshaped = faceImg.reshape(0, 1);//转换成一行N列的矩阵
reshaped.row(0).copyTo(data_mat.row(index));
labels_mat.at<int>(index, 0) = i -1;
index++;//更新data_mat、labels_mat的行号索引
}
}
我这里比较懒,直接用文件夹分类:
vector<vector<String> > imgByDir;//按文件夹存储图片路径的向量,每个文件夹代表不同的label
......
labels_mat.at<int>(index, 0) = i -1;
PCA的原理可以参考这一篇文章:
PCA的数学原理
opencv的pca实现代码:
//3. PCA降维:1. PCA之前先做归一化处理 2.PCA模型可以保存
int K = nImgNum*0.5;//PCA主成分维数,需要小于样本数,大于样本数时等于样本数
PCA pca(data_mat, Mat(), PCA::DATA_AS_ROW, K);//
//TODO:把PCA模型保存
FileStorage fs("PCA.xml", FileStorage::WRITE);
pca.write(fs);
fs.release(); // flush
Mat projectedMat = pca.project(data_mat);//映射 降维,降维后的矩阵传给SVM训练
//Mat back = pca.backProject(projectedMat);//从K维矩阵反映射到原来的维数
//4. 创建分类器并设置参数
Ptr<SVM> SVM_params = SVM::create();
SVM_params->setType(SVM::C_SVC);
SVM_params->setKernel(SVM::LINEAR);//核函数
//SVM_params->setDegree(10.0);
//SVM_params->setGamma(0.09);
//SVM_params->setCoef0(1.0);
SVM_params->setC(2.0);//惩罚系数,不能太小:欠拟合,不能太多:过拟合,
//SVM_params->setNu(0.5);
//SVM_params->setP(1.0);
SVM_params->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER + TermCriteria::EPS, 1000, 0.01));
// 训练分类器
SVM_params->train(projectedMat, ROW_SAMPLE, labels_mat);
// 保存模型
SVM_params->save("PCA_SVM.xml");
在上面的代码中直接预测:
//5. 模型预测
char result[512];
vector<vector<String> > img_tst_path;
int testNum = 0;//测试样本数量
int wrongNum = 0;//预测错误数量 计算准确率
img_tst_path = getImgPath("PathForTest.txt", testNum);
cout << "共有测试样本个数为:" << testNum << endl;
ofstream predict_txt("SVM_PREDICT.txt");//把预测结果存储在这个文本中
for (string::size_type j = 0; j != img_tst_path.size(); j++){
for (size_t i = 0; i < img_tst_path[j].size(); i++)
{
Mat img = imread(img_tst_path[j][i].c_str(), 0);
if (img.empty()){
cout << " can not load the image: " << img_tst_path[j][i].c_str() << endl;
continue;
}
Mat faceImg = getFaceImg(img, face_cascade);
//imshow("测试图片", faceImg);
//waitKey(1);
Mat test = Mat(1, K, CV_32FC1);
Mat test1 = faceImg.reshape(0, 1);
pca.project(test1, test);
int ret = SVM_params->predict(test);//检测结果
if (ret != j-1)wrongNum++;//我这里训练和测试的文件顺序是一样的,第j个文件夹就代表它的类别是j,可以依据此来判断是不是预测正确
sprintf_s(result, "%s %d\r", img_tst_path[j][i].c_str(), ret);
predict_txt << result; //输出检测结果到文本
}
}
float accuracyRate = (float)(testNum - wrongNum) / (float)testNum;
cout << "预测正确率:" << accuracyRate << endl;
predict_txt.close();
system("pause");
从xml文件中加载模型预测并从摄像头实时识别:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "windows.h"
#include "fstream"
using namespace std;
using namespace cv;
using namespace cv::ml;
#define Height 88
#define Width 88
Mat getFaceImg(Mat src, CascadeClassifier cascade, Rect& face){
//人脸检测
Mat faceImg = src;
vector<Rect> faces;
//equalizeHist(src,src);//直方图均衡化
cascade.detectMultiScale(src, faces, 1.1, 2, 0 | CASCADE_SCALE_IMAGE, Size(30, 30));
//裁剪出人脸
for (size_t f = 0; f < faces.size(); f++){
if (faces[f].width > 90){
faceImg = src(faces[f]);
face = faces[f];
break;
}
}
//统一图片大小
Mat resized;
resize(faceImg, resized, Size(Width, Height));
normalize(resized, resized, 0, 255, NORM_MINMAX);//归一化
return resized;
}
int main()
{
//【0】加载PCA、SVM、人脸检测xml
String face_cascade_name = "src/haarcascade_frontalface_alt.xml";
CascadeClassifier face_cascade;
if (!face_cascade.load(face_cascade_name)){ printf("--(!)Error loading\n"); return -1; };
cout << "load src/haarcascade_frontalface_alt.xml pass!" << std::endl;
Ptr<ml::SVM>svm = ml::SVM::load("src/PCA_SVM.xml");//加载训练好的xml文件,
cout << "load src/PCA_SVM.xml pass!" << std::endl;
PCA pca;
FileStorage fs("src/PCA.xml", FileStorage::READ);
pca.read(fs.root());
fs.release();
cout << "load src/PCA.xml pass!" << std::endl;
//【1】从摄像头读入视频
VideoCapture capture(0);
//【2】循环显示每一帧
cout << "press C to exit " << std::endl;
char name[100];
while (1)
{
Mat frame; //定义一个Mat变量,用于存储每一帧的图像
capture >> frame; //读取当前帧
Mat gray;
cvtColor(frame,gray,CV_RGB2GRAY);
Rect faces;
Mat face = getFaceImg(gray, face_cascade, faces);
Mat reshaped = face.reshape(0, 1);//转换成一行N列的矩阵
Mat project = pca.project(reshaped);
int ret = svm->predict(project);
cout << "The predict result is : " << ret << endl;
Point center(faces.x + faces.width / 2, faces.y + faces.height / 2);
ellipse(frame, center, Size(faces.width / 2, faces.height / 2 + 16), 0, 0, 360, Scalar(200, 10, 10), 2, 8, 0);
Point org(faces.x+10,faces.y-35 );
sprintf_s(name, "person:%d", ret);
putText(frame, name, org, FONT_HERSHEY_SIMPLEX, 0.8,Scalar(10, 10, 200),2);
imshow("读取视频", frame); //显示当前帧
int c = waitKey(1);
if ((char)c == 'c') { break; }
}
return 0;
}
当输入一个没有经过训练的类别去预测时,svm始终会返回一个预测值,所以当我们想实现输入一张陌生人的脸时,识别为-1,就需要训练一个负样本类别。本文将负样本数据加入分类训练后输入一张陌生人的脸时,能正确识别为-1,但是还存在问题,有可能是我的负样本数据的问题
作者:yymbyc 于 2020/03/03