利用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
目錄
- 準備和讀取數據
- 數據前處理
- 數據分類
- PCA降維
- SVM分類
- 預測
- 關於非訓練類別預測結果隨機的處理辦法
- 參考鏈接
1. 準備和讀取數據
1.1 目錄介紹
首先給大家看一下我的VS目錄結構,DataBase中就是存放的數據集,其中的圖像都是未經處理過的含有人臉的圖片。
DataBase文件夾如下圖:
1.2 數據讀取
本文采用讀取TXT文件的方式,依次讀入所需圖片數據:
PathForTrain.txt包含了訓練集的路徑:
PathForTest.txt包含了測試機的路徑:
1.3 圖像讀取程序
首先實現一個根據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 <<
2. 數據前處理
本文的目的是實現人臉識別,所以首先需要進行人臉檢測,把人臉數據送去降維和分類,這裏用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的行號索引
}
}
3. 數據分類
我這裏比較懶,直接用文件夾分類:
vector<vector<String> > imgByDir;//按文件夾存儲圖片路徑的向量,每個文件夾代表不同的label
......
labels_mat.at<int>(index, 0) = i -1;
4. 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維矩陣反映射到原來的維數
5. SVM分類
//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");
6. 預測
在上面的代碼中直接預測:
//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 <stdio.h>
#include <time.h>
#include <math.h>
#include <opencv2/opencv.hpp>
#include <opencv/cv.h>
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/ml/ml.hpp>
#include <io.h>
#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;
}
7. 關於非訓練類別預測結果隨機的處理辦法
當輸入一個沒有經過訓練的類別去預測時,svm始終會返回一個預測值,所以當我們想實現輸入一張陌生人的臉時,識別爲-1,就需要訓練一個負樣本類別。本文將負樣本數據加入分類訓練後輸入一張陌生人的臉時,能正確識別爲-1,但是還存在問題,有可能是我的負樣本數據的問題
8. 參考鏈接
- 東城青年:基於PCA和SVM的人臉識別
- 邁克老狼2012:OpenCV學習(35) OpenCV中的PCA算法
- 朱銘德:PCA降維(Opencv,C++)
- 張洋:PCA的數學原理
作者:yymbyc 於 2020/03/03