基於OpenCV3實現人臉識別(實踐篇)
實踐總結:
- 首先了解做人臉識別的步驟
- 各個演算法後面的原理
- 原理背後的相關知識的瞭解
- 人臉識別專案總遇到的問題
由於篇幅原因,後面一篇寫各個演算法背後的原理,原理背後的相關知識的瞭解,人臉識別專案總遇到的問題
首先感謝:
正 文
1首先了解做人臉識別的步驟
資料收集和預處理、訓練模型、人臉識別三個部分
- 資料收集和預處理
(1)下載資料集
本次用的資料集是opencv給出的教程裡面的第一個資料集:
可以用imread()函式讀出pgm看看各圖效果。
(2)準備識別人臉的資料集
拍照程式
#include <opencv2\opencv.hpp> #include <vector> #include <iostream> #include<stdio.h> //#include <stdio.h> //#include <cv.h> using namespace std; using namespace cv; int main() { CascadeClassifier cascada; cascada.load("haarcascade_frontalface_alt2.xml"); VideoCapture cap(0); Mat frame, myFace; int pic_num = 1; while (1) { //攝像頭讀影象 cap >> frame; vector<Rect> faces;//vector容器存檢測到的faces Mat frame_gray; cvtColor(frame, frame_gray, COLOR_BGR2GRAY);//轉灰度化,減少運算 cascada.detectMultiScale(frame_gray, faces, 1.1, 4, CV_HAAR_DO_ROUGH_SEARCH, Size(70, 70), Size(1000, 1000)); printf("檢測到人臉個數:%d\n", faces.size()); //1.frame_gray表示的是要檢測的輸入影象 2.faces表示檢測到的人臉目標序列,3. 1.1表示每次影象尺寸減小的比例 //4. 4表示每一個目標至少要被檢測到3次才算是真的目標(因為周圍的畫素和不同的視窗大小都可以檢測到人臉表示每一個目標至少要被檢測到3次才算是真的目標(因為周圍的畫素和不同的視窗大小都可以檢測到人臉 /*5.flags–要麼使用預設值,要麼使用CV_HAAR_DO_CANNY_PRUNING, 函式將會使用Canny邊緣檢測來排除邊緣過多或過少的區域, 因為這些區域通常不會是人臉所在區域;opencv3 以後都不用這個引數了*/ //6. Size(100, 100)為目標的最小尺寸 一般為30*30 是最小的了 也夠了 //7. Size(500, 500)為目標的最大尺寸 其實可以不用這個,opencv會自動去找這個最大尺寸 //適當調整5,6,7兩個引數可以用來排除檢測結果中的干擾項。 //識別到的臉用矩形圈出 for (int i = 0; i < faces.size(); i++) { rectangle(frame, faces[i], Scalar(255, 0, 0), 2, 8, 0); } //當只有一個人臉時,開始拍照 if (faces.size() == 1) { Mat faceROI = frame_gray(faces[0]);//在灰度圖中將圈出的臉所在區域裁剪出 //cout << faces[0].x << endl;//測試下face[0].x resize(faceROI, myFace, Size(92, 112));//將興趣域size為92*112 putText(frame, to_string(pic_num), faces[0].tl(), 3, 1.2, (0, 0, 225), 2, 0);//在 faces[0].tl()的左上角上面寫序號 string filename = format("%d.jpg", pic_num); //存放在當前專案資料夾以1-10.jpg 命名,format就是轉為字串 imwrite(filename, myFace);//存在當前目錄下 imshow(filename, myFace);//顯示下size後的臉 waitKey(500);//等待500us destroyWindow(filename);//:銷燬指定的視窗 pic_num++;//序號加1 if (pic_num == 11) { return 0;//當序號為11時退出迴圈 } } int c = waitKey(10); if ((char)c == 27) { break; } //10us內輸入esc則退出迴圈 imshow("frame", frame);//顯示視訊流 waitKey(100);//等待100us } return 0; }
預處理
在得到自己的人臉照片之後,還需要對這些照片進行一些預處理才能拿去訓練模型。所謂預處理,其實就是檢測並分割出人臉,並改變人臉的大小與下載的資料集中圖片大小一致。
呼叫opencv訓練好的分類器和自帶的檢測函式檢測人臉人眼等的步驟簡單直接:
- 1.載入分類器,當然分類器事先要放在工程目錄中去。分類器本來的位置是在*\opencv\sources\data\haarcascades(harr分類器,也有其他的可以用,也可以自己訓練)
- 2.呼叫detectMultiScale()函式檢測,調整函式的引數可以使檢測結果更加精確。
- 3.把檢測到的人臉等用矩形(或者圓形等其他圖形)畫出來。
其實上面第一份程式碼就包括了上面的拍照和預處理了。
至此,我們就得到和ORL人臉資料庫人臉大小一致的自己的人臉資料集。然後我們把自己的作為第41個人,在我們下載的人臉資料夾下建立一個s41的子資料夾,把自己的人臉資料放進去。就成了這樣下面這樣,最後一個資料夾裡面是我自己的頭像照片:
這裡有一點值得注意:儲存的影象格式是*.jpg的,而不是跟原資料集一樣是*.pgm的。經測試仍然可以訓練出可以正確識別我和其他準備識別的人臉的模型來。但是如果大小不一致會報錯,所以大小:92*112。
- 模型訓練
csv檔案的生成
當我們寫人臉模型的訓練程式的時候,我們需要讀取人臉和人臉對應的標籤。直接在資料庫中讀取顯然是低效的。所以我們用csv檔案讀取。csv檔案中包含兩方面的內容,一是每一張圖片的位置所在,二是每一個人臉對應的標籤,就是為每一個人編號。這個at.txt就是我們需要的csv檔案。生成之後它裡面是這個樣子的:
前面是圖片的位置,後面是圖片所屬人臉的人的標籤。
要生成這樣一個檔案直接用手工的方式一個一個輸入顯然不可取的,畢竟這裡有400多張圖片。而且這種重複性的工作估計也沒人想去做。所以我們可以用命令列的方式簡化工作量;或者用opencv自帶的Python指令碼來自動生成。
命令列方式是這樣的。比如我的資料集在F:\FaceRecognitionwithOpenCV\FaceRecognition\FaceRecognition\orl_faces資料夾下面,我就用下面兩行命令:(這裡盜用了一張部落格大神的圖片啦,感謝,如下)
然後資料集資料夾下面就多出了一個at.txt檔案,但是現在是隻有路徑沒有標籤的。標籤需要手動敲上去。。。也挺麻煩的。
好在opencv教程裡面為我們提供了自動生成csv檔案的指令碼。
路徑類似這樣:D:\Program Files\opencv340\opencv_contrib-3.4.0\modules\face\samples\etc\create_csv.py。
我不知道怎麼用命令列引數的形式執行Python指令碼,所以只能把程式碼裡面的BASE_PATH手動的改成自己的資料集路徑,改完大致是這樣:
然後執行這個指令碼就可以生成一個既有路徑又有標籤的at.txt了。
當然:你也可以自己手動製作,用別人的csv檔案路徑換成自己的,但注意空格、符號、別搞錯了。(我就是用這種方法)
- 訓練模型
現在資料集、csv檔案都已經準備好了。接下來要做的就是訓練模型了。
這裡我們用到了opencv的Facerecognizer類。opencv中所有的人臉識別模型都是來源於這個類,這個類為所有人臉識別演算法提供了一種通用的介面。文件裡的一個小段包含了我們接下來要用到的幾個函式:
OpenCV 自帶了三個人臉識別演算法:Eigenfaces(特徵臉),Fisherfaces 和區域性二進位制模式直方圖 (LBPH)。這裡先不去深究這些演算法的具體內容,直接用就是了。如果有興趣可以去看相關論文。接下來就分別訓練這三種人臉模型。這個時候就能體現出Facerecognizer類的強大了。因為每一種模型的訓練只需要三行程式碼:
注意區別:opencv3呼叫API的語句。EigenFaceRecognizer::create(),而Eigenfaces、fisherfaces、區域性二進位制模式直方圖
當然在這之前要先把之前圖片和標籤提取出來。這時候就是at.txt派上用場的時候了。
在模型訓練好之後我們拿資料集中的最後一張圖片做一個測試,看看結果如何。
由於本來的資料集中是40個人,加上自己和同學的人臉集就是42個。標籤是從1開始標的,所以在這裡同學是第42個人。也即是說Actual class應該42。Predicted class也應該是42才說明預測準確.測試了三個模型。所有三個結果。
模型訓練的全部程式碼:
#include<opencv2\face\facerec.hpp> //opencv3需要
#include<opencv2\core.hpp>
#include<opencv2\face.hpp>
#include<opencv2\highgui.hpp>
#include<opencv2\imgproc.hpp>
#include <math.h>
//使用void read_csv()這個函式必須的三個標頭檔案
#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);//c_str()函式可用可不用,無需返回一個標準C型別的字串
if (!file)
{
string error_message = "No valid input file was given, please check the given filename.";
CV_Error(CV_StsBadArg, error_message);
}
string line, path, classlabel;
while (getline(file, line)) //從文字檔案中讀取一行字元,未指定限定符預設限定符為“/n”
{
stringstream liness(line);//這裡採用stringstream主要作用是做字串的分割
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()
{
//讀取你的CSV檔案路徑.
//string fn_csv = string(argv[1]);
string fn_csv = "at.txt";
// 2個容器來存放影象資料和對應的標籤
vector<Mat> images;
vector<int> labels;
// 讀取資料. 如果檔案不合法就會出錯
// 輸入的檔名已經有了.
try
{
read_csv(fn_csv, images, labels); //從csv檔案中批量讀取訓練資料
}
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);
}
for (int i = 0; i < images.size(); i++)
{
if (images[i].size() != Size(92, 112))
{
cout << i << endl;
cout << images[i].size() << endl;
}
}
// 下面的幾行程式碼僅僅是從你的資料集中移除最後一張圖片,作為測試圖片
//[gm:自然這裡需要根據自己的需要修改,他這裡簡化了很多問題]
Mat testSample = images[images.size() - 1];
int testLabel = labels[labels.size() - 1];
images.pop_back();//刪除最後一張照片,此照片作為測試圖片
labels.pop_back();//刪除最有一張照片的labels
// 下面幾行建立了一個特徵臉模型用於人臉識別,
// 通過CSV檔案讀取的影象和標籤訓練它。
// T這裡是一個完整的PCA變換
//如果你只想保留10個主成分,使用如下程式碼
// cv::createEigenFaceRecognizer(10);
//
// 如果你還希望使用置信度閾值來初始化,使用以下語句:
// cv::createEigenFaceRecognizer(10, 123.0);
//
// 如果你使用所有特徵並且使用一個閾值,使用以下語句:
// cv::createEigenFaceRecognizer(0, 123.0);
//建立一個PCA人臉分類器,暫時命名為model吧,建立完成後
//呼叫其中的成員函式train()來完成分類器的訓練
Ptr<BasicFaceRecognizer> model = EigenFaceRecognizer::create();
model->train(images, labels);
model->save("MyFacePCAModel.xml");//儲存路徑可自己設定,但注意用“\\”
Ptr<BasicFaceRecognizer> model1 = FisherFaceRecognizer::create();
model1->train(images, labels);
model1->save("MyFaceFisherModel.xml");
Ptr<LBPHFaceRecognizer> model2 = LBPHFaceRecognizer::create();
model2->train(images, labels);
model2->save("MyFaceLBPHModel.xml");
// 下面對測試影象進行預測,predictedLabel是預測標籤結果
//注意predict()入口引數必須為單通道灰度影象,如果影象型別不符,需要先進行轉換
//predict()函式返回一個整形變數作為識別標籤
int predictedLabel = model->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_message = format("Predicted class = %d / Actual class = %d.", predictedLabel, 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_message << endl;
cout << result_message1 << endl;
cout << result_message2 << endl;
getchar();
//waitKey(0);
return 0;
}
- 識別人臉
簡單說下流程:
1.開啟攝像頭。
2.載入人臉檢測器,載入人臉模型。
3.人臉檢測
4.把檢測到的人臉與人臉模型裡面的對比,找出這是誰的臉。(背後的原理可不這麼簡單)後面涉及的原理統統給上。
5.如果人臉是自己拍照的人臉,顯示自己的名字。
先看結果:
程式碼:
#include<opencv2\opencv.hpp>
#include<opencv2\face.hpp>
#include<opencv2\core\core.hpp>
#include<opencv2\face\facerec.hpp>
#include <fstream>
#include <sstream>
#include<math.h>
using namespace std;
using namespace cv;
using namespace cv::face;
RNG g_rng(12345);
Ptr<FaceRecognizer> model;
int Predict(Mat src_image) //識別圖片
{
Mat face_test;
int predict = 0;
//擷取的ROI人臉尺寸調整
if (src_image.rows >= 120)
{
//改變影象大小,使用雙線性差值
resize(src_image, face_test, Size(92, 112));
}
//判斷是否正確檢測ROI
if (!face_test.empty())
{
//測試影象應該是灰度圖
predict = model->predict(face_test);
}
cout << predict << endl;
return predict;
}
int main()
{
VideoCapture cap(0); //開啟預設攝像頭
if (!cap.isOpened())
{
return -1;
}
Mat frame;
Mat gray;
//這個分類器是人臉檢測所用
CascadeClassifier cascade;
bool stop = false;
//訓練好的檔名稱,放置在可執行檔案同目錄下
cascade.load("haarcascade_frontalface_alt2.xml");//感覺用lbpcascade_frontalface效果沒有它好,注意哈!要是正臉
model = FisherFaceRecognizer::create();
//1.載入訓練好的分類器
model->read("MyFaceFisherModel.xml");// opencv2用load
//3.利用攝像頭採集人臉並識別
while (1)
{
cap >> frame;
vector<Rect> faces(0);//建立用於存放人臉的向量容器
cvtColor(frame, gray, CV_RGB2GRAY);//測試影象必須為灰度圖
equalizeHist(gray, gray); //變換後的影象進行直方圖均值化處理
//檢測人臉
cascade.detectMultiScale(gray, faces,
1.1, 4, 0
//|CV_HAAR_FIND_BIGGEST_OBJECT
| CV_HAAR_DO_ROUGH_SEARCH,
//| CV_HAAR_SCALE_IMAGE,
Size(30, 30), Size(500, 500));
Mat* pImage_roi = new Mat[faces.size()]; //定以陣列
Mat face;
Point text_lb;//文字寫在的位置
//框出人臉
string str;
for (int i = 0; i < faces.size(); i++)
{
pImage_roi[i] = gray(faces[i]); //將所有的臉部儲存起來
text_lb = Point(faces[i].x, faces[i].y);
if (pImage_roi[i].empty())
continue;
switch (Predict(pImage_roi[i])) //對每張臉都識別
{
case 41:str = "HuangHaiNa"; break;
case 42:str = "XuHaoRan"; break;
case 43:str = "HuangLuYao"; break;
default: str = "Error"; break;
}
Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));//所取的顏色任意值
rectangle(frame, Point(faces[i].x, faces[i].y), Point(faces[i].x + faces[i].width, faces[i].y + faces[i].height), color, 1, 8);//放入快取
putText(frame, str, text_lb, FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255));//新增文字
}
delete[]pImage_roi;
imshow("face", frame);
waitKey(200);
}
return 0;
}