以下為EMGU Multiple Face Recognition using PCA and Parallel Optimisation筆記
在此, 我會試著用 libemgucv-windows-universal-cuda-2.4.10.1940版本測試,
因為在上去3.0版本並不支援FaceRecognizer
==========================================================
如果你是第一次使用Emgu CV, 請參考Creating Your First EMGU Image Processing Project article
人臉辨識是以Multiple face detection and recognition in real time為基礎
PCA analysis為人臉辨識主要基礎理論,針對理論及其應用進行學習, 原始碼也進行優化並加強其方便和實用性
2.4.9版更新
CascadeClassifier類別: 用來取得人臉定位
FaceRecognizer:允許以Eigen, Fisher, LBPH(Local Binary Pattern Histogram)作為特徵, 並進行分類
對於陌生人(未加入訓練的人), FaceRecognizer有個bug, 後續會進行說明
看文章前先看一下人機介面
EMGU the FaceRecognizer如何作用?
上面應該是下圖的繼承關係, 其中Egien的門檻值(threshold)不同於Fisher和LBPH的門檻值
The Eigen Classifier
FaceRecognizer recognizer = new EigenFaceRecognizer(num_components, threshold);
num_components參數為主軸成分個數, 原作者說這個設定沒有一訂準則, 原則上建議為80個就夠用, 我建議是要看不同主軸個數選擇下, 每個人臉表示的訊號佔原始訊號的能量比例而定, 如果解釋率達99.9%應該部會有太大問題
threshold參數為人臉資料庫的門檻值(差異上限值, distance), 如果超過該門檻值都會被視為陌生人(unkown), 所以如果設定太低, 就變成bug, 全部都視為unkown…@@, 所以原作者將此bug進行處理, 將門檻值設定一個很大的初始值, 這樣所有訓練樣本都不會是unkown, 當辨識完成會傳回Eigen_Distance, 以便使用者決定unknown的門檻值
1: FaceRecognizer recognizer = new EigenFaceRecognizer(80, double.PositiveInfinity);
以下為Reconise副程式, 輸入一張影像及門檻值
1: public string Recognise(Image<Gray, Byte> Input_image, int Eigen_Thresh = -1)
2: {
3: if (_IsTrained)
4: {
5: FaceRecognizer.PredictionResult ER = recognizer.Predict(Input_image);
6:
7: .....
8: //Only use the post threshold rule if we are using an Eigen Recognizer
9: //since Fisher and LBHP threshold set during the constructor will work correctly
10: switch (Recognizer_Type)
11: {
12: case ("EMGU.CV.EigenFaceRecognizer"):
13: if (Eigen_Distance > Eigen_threshold) return Eigen_label;
14: else return "Unknown";
15: case ("EMGU.CV.LBPHFaceRecognizer"):
16: case ("EMGU.CV.FisherFaceRecognizer"):
17: default:
18: return Eigen_label; //the threshold set in training controls unknowns
19: }
20: }
21: ......
22: }
The Fisher Classifier
FaceRecognizer recognizer = new FisherFaceRecognizer(num_components, threshold);
num_components為Linear Discriminant Analysis主軸個數, 原作者建議設定為0, 即保留全部主軸個數(即大於等於訓練樣本個數-1)
threshold為陌生人門檻值, 如果Eigen distance大於該門檻值, 則視為陌生人, 預設值為3500
The Local Binary Pattern Histogram (LBPH) Classifier
FaceRecognizer recognizer = new LBPHFaceRecognizer(radius, neighbors, grid_x, grid_y, threshold);
radius: 建立Circular Local Pattern半徑
neighbors: 鄰居點個數, 建議值為8, 即一般熟知的九宮格
grid_x和grid_y: 水平和垂直網格點數, 數值越大則特徵越細緻但維度也同時增加(計算量變大)
threshold: 為陌生人門檻值, 如果Eigen distance大於該門檻值, 則視為陌生人回傳-1, 和FisherFace相同
1: FaceRecognizer recognizer = new LBPHFaceRecognizer(1, 8, 8, 8, 100);//50
===================================================================
以下為PCA處理步驟, 建議參考Matthre & Alex的Face Recognition Using Eigenfaces[5]
- Stage 1: Subtract the Mean of the data from each variable (our adjusted data)
- Stage 2: Calculate and form a covariance Matrix
- Stage 3: Calculate Eigenvectors and Eigenvalues from the covariance Matrix
- Stage 4: Chose a Feature Vector (a fancy name for a matrix of vectors)
- Stage 5: Multiply the transposed Feature Vectors by the transposed adjusted data
取臉的列(row)方向, 將全部人臉同一列的數值進行平均, 譬如第一列, 將100張人臉的同一列像數值取平均,如果是取行方向也可以, 記得後續covariance matrix計算記得調整
假設每個人臉2維影像資料轉成1維, 則一張人臉100×100影像大小, 則可以變成10,000x1的行向量, 再針對100張人臉構成矩陣X(10,000x100),sum(X, 2)/100, 得到平均影像其維度為10,000x1
觀察每張影像資料和平均影像的變化程度
====================================================================
Classifier_Train.cs
架構如下
1: Vaiables
2:
3: Constructors
4:
5: Public
6:
7: Private
先看Constructors段落, 如下
一個預設建構子Classifier_Train(), 另一個傳入要訓練的目錄夾路徑
預設路徑: LoadTrainingData(應用程式執行目錄\\TrainedFaces)
1: public Classifier_Train()
2: {
3: _IsTrained = LoadTrainingData(Application.StartupPath + "\\TrainedFaces");
4: }
user指定目錄Training_Folder
1: public Classifier_Train(string Training_Folder)
2: {
3: _IsTrained = LoadTrainingData(Training_Folder);
4: }
接下來看一下Varaibles段落
FaceRecognizer類別只在2.4.x版支援@@
1: //Eigen
2: //EigenObjectRecognizer recognizer;
3: FaceRecognizer recognizer;
4:
5: //training variables
6: List<Image<Gray, byte>> trainingImages = new List<Image<Gray, byte>>();//Images
7: //TODO: see if this can be combined in Ditionary format this will remove support for old data
8: List<string> Names_List = new List<string>(); //labels
9: List<int> Names_List_ID = new List<int>();
10: int ContTrain, NumLabels;
11: float Eigen_Distance = 0;
12: string Eigen_label;
13: int Eigen_threshold = 2000;
14:
15: //Class Variables
16: string Error;
17: bool _IsTrained = false;
18:
19: public string Recognizer_Type = "EMGU.CV.EigenFaceRecognizer";
List<Image<Gray, byte>> trainingImages 主要將訓練影像堆疊在此變數中
List<string> Names_List 堆疊標籤名稱, 即標記人臉名稱
List<int> Names_List_ID 推疊名稱ID
接下來看 Public段落
1: /// <summary>
2: /// Retrains the recognizer witout resetting variables like recognizer type.
3: /// </summary>
4: /// <returns></returns>
5: public bool Retrain()
6: {
7: return _IsTrained = LoadTrainingData(Application.StartupPath + "\\TrainedFaces");
8: }
9: /// <summary>
10: /// Retrains the recognizer witout resetting variables like recognizer type.
11: /// Takes String input to a different location for training data.
12: /// </summary>
13: /// <returns></returns>
14: public bool Retrain(string Training_Folder)
15: {
16: return _IsTrained = LoadTrainingData(Training_Folder);
17: }
辨識核心
1: /// <summary>
2: /// Recognise a Grayscale Image using the trained Eigen Recogniser
3: /// </summary>
4: /// <param name="Input_image"></param>
5: /// <returns></returns>
6: public string Recognise(Image<Gray, byte> Input_image, int Eigen_Thresh = -1)
7: {
8: if (_IsTrained)
9: {
10: FaceRecognizer.PredictionResult ER = recognizer.Predict(Input_image);
11:
12: if (ER.Label == -1)
13: {
14: Eigen_label = "Unknown";
15: Eigen_Distance = 0;
16: return Eigen_label;
17: }
18: else
19: {
20: Eigen_label = Names_List[ER.Label];
21: Eigen_Distance = (float)ER.Distance;
22: if (Eigen_Thresh > -1) Eigen_threshold = Eigen_Thresh;
23:
24: //Only use the post threshold rule if we are using an Eigen Recognizer
25: //since Fisher and LBHP threshold set during the constructor will work correctly
26: switch (Recognizer_Type)
27: {
28: case ("EMGU.CV.EigenFaceRecognizer"):
29: if (Eigen_Distance > Eigen_threshold) return Eigen_label;
30: else return "Unknown";
31: case ("EMGU.CV.LBPHFaceRecognizer"):
32: case ("EMGU.CV.FisherFaceRecognizer"):
33: default:
34: return Eigen_label; //the threshold set in training controls unknowns
35: }
36:
37:
38:
39:
40: }
41:
42: }
43: else return "";
44: }
設定Eigen門檻值
1: /// <summary>
2: /// Sets the threshold confidence value for string Recognise(Image<Gray, byte> Input_image) to be used.
3: /// </summary>
4: public int Set_Eigen_Threshold
5: {
6: set
7: {
8: //NOTE: This is still not working correctley
9: //recognizer.EigenDistanceThreshold = value;
10: Eigen_threshold = value;
11: }
12: }
取得辨識結果對應的標籤(名稱)
1:
2: /// <summary>
3: /// Returns a string containg the recognised persons name
4: /// </summary>
5: public string Get_Eigen_Label
6: {
7: get
8: {
9: return Eigen_label;
10: }
11: }
取得Eigen Distance(差異度)
1: /// <summary>
2: /// Returns a float confidence value for potential false clasifications
3: /// </summary>
4: public float Get_Eigen_Distance
5: {
6: get
7: {
8: //get eigenDistance
9: return Eigen_Distance;
10: }
11: }
取得錯誤訊息
1: /// <summary>
2: /// Returns a string contatining any error that has occured
3: /// </summary>
4: public string Get_Error
5: {
6: get { return Error; }
7: }
儲存recognizer至某個參數檔案(xml)
1: /// <summary>
2: /// Saves the trained Eigen Recogniser to specified location
3: /// </summary>
4: /// <param name="filename"></param>
5: public void Save_Eigen_Recogniser(string filename)
6: {
7: recognizer.Save(filename);
8:
9: //save label data as this isn't saved with the network
10: string direct = Path.GetDirectoryName(filename);
11: FileStream Label_Data = File.OpenWrite(direct + "/Labels.xml");
12: using (XmlWriter writer = XmlWriter.Create(Label_Data))
13: {
14: writer.WriteStartDocument();
15: writer.WriteStartElement("Labels_For_Recognizer_sequential");
16: for (int i = 0; i < Names_List.Count; i++)
17: {
18: writer.WriteStartElement("LABEL");
19: writer.WriteElementString("POS", i.ToString());
20: writer.WriteElementString("NAME", Names_List[i]);
21: writer.WriteEndElement();
22: }
23:
24: writer.WriteEndElement();
25: writer.WriteEndDocument();
26: }
27: Label_Data.Close();
28: }
載入參數檔案至recognizer
1: /// <summary>
2: /// Loads the trained Eigen Recogniser from specified location
3: /// </summary>
4: /// <param name="filename"></param>
5: public void Load_Eigen_Recogniser(string filename)
6: {
7: //Lets get the recogniser type from the file extension
8: string ext = Path.GetExtension(filename);
9: switch (ext)
10: {
11: case (".LBPH"):
12: Recognizer_Type = "EMGU.CV.LBPHFaceRecognizer";
13: recognizer = new LBPHFaceRecognizer(1, 8, 8, 8, 100);//50
14: break;
15: case (".FFR"):
16: Recognizer_Type = "EMGU.CV.FisherFaceRecognizer";
17: recognizer = new FisherFaceRecognizer(0, 3500);//4000
18: break;
19: case (".EFR"):
20: Recognizer_Type = "EMGU.CV.EigenFaceRecognizer";
21: recognizer = new EigenFaceRecognizer(80, double.PositiveInfinity);
22: break;
23: }
24:
25: //introduce error checking
26: recognizer.Load(filename);
27:
28: //Now load the labels
29: string direct = Path.GetDirectoryName(filename);
30: Names_List.Clear();
31: if (File.Exists(direct + "/Labels.xml"))
32: {
33: FileStream filestream = File.OpenRead(direct + "/Labels.xml");
34: long filelength = filestream.Length;
35: byte[] xmlBytes = new byte[filelength];
36: filestream.Read(xmlBytes, 0, (int)filelength);
37: filestream.Close();
38:
39: MemoryStream xmlStream = new MemoryStream(xmlBytes);
40:
41: using (XmlReader xmlreader = XmlTextReader.Create(xmlStream))
42: {
43: while (xmlreader.Read())
44: {
45: if (xmlreader.IsStartElement())
46: {
47: switch (xmlreader.Name)
48: {
49: case "NAME":
50: if (xmlreader.Read())
51: {
52: Names_List.Add(xmlreader.Value.Trim());
53: }
54: break;
55: }
56: }
57: }
58: }
59: ContTrain = NumLabels;
60: }
61: _IsTrained = true;
62:
63: }
資源釋放
1: /// Dispose of Class call Garbage Collector
2: /// </summary>
3: public void Dispose()
4: {
5: recognizer = null;
6: trainingImages = null;
7: Names_List = null;
8: Error = null;
9: GC.Collect();
10: }
Private段落
LoadTrainingData載入全部訓練照片及對應名稱(標籤)
1: /// <summary>
2: /// Loads the traing data given a (string) folder location
3: /// </summary>
4: /// <param name="Folder_location"></param>
5: /// <returns></returns>
6: private bool LoadTrainingData(string Folder_location)
7: {
8: if (File.Exists(Folder_location +"\\TrainedLabels.xml"))
9: {
10: try
11: {
12: //message_bar.Text = "";
13: Names_List.Clear();
14: Names_List_ID.Clear();
15: trainingImages.Clear();
16: FileStream filestream = File.OpenRead(Folder_location + "\\TrainedLabels.xml");
17: long filelength = filestream.Length;
18: byte[] xmlBytes = new byte[filelength];
19: filestream.Read(xmlBytes, 0, (int)filelength);
20: filestream.Close();
21:
22: MemoryStream xmlStream = new MemoryStream(xmlBytes);
23:
24: using (XmlReader xmlreader = XmlTextReader.Create(xmlStream))
25: {
26: while (xmlreader.Read())
27: {
28: if (xmlreader.IsStartElement())
29: {
30: switch (xmlreader.Name)
31: {
32: case "NAME":
33: if (xmlreader.Read())
34: {
35: Names_List_ID.Add(Names_List.Count); //0, 1, 2, 3....
36: Names_List.Add(xmlreader.Value.Trim());
37: NumLabels += 1;
38: }
39: break;
40: case "FILE":
41: if (xmlreader.Read())
42: {
43: //PROBLEM HERE IF TRAININGG MOVED
44: trainingImages.Add(new Image<Gray, byte>(Application.StartupPath + "\\TrainedFaces\\" + xmlreader.Value.Trim()));
45: }
46: break;
47: }
48: }
49: }
50: }
51: ContTrain = NumLabels;
52:
53: if (trainingImages.ToArray().Length != 0)
54: {
55:
56: //Eigen face recognizer
57: //Parameters:
58: // num_components – The number of components (read: Eigenfaces) kept for this Prinicpal
59: // Component Analysis. As a hint: There’s no rule how many components (read: Eigenfaces)
60: // should be kept for good reconstruction capabilities. It is based on your input data,
61: // so experiment with the number. Keeping 80 components should almost always be sufficient.
62: //
63: // threshold – The threshold applied in the prediciton. This still has issues as it work inversly to LBH and Fisher Methods.
64: // if you use 0.0 recognizer.Predict will always return -1 or unknown if you use 5000 for example unknow won't be reconised.
65: // As in previous versions I ignore the built in threhold methods and allow a match to be found i.e. double.PositiveInfinity
66: // and then use the eigen distance threshold that is return to elliminate unknowns.
67: //
68: //NOTE: The following causes the confusion, sinc two rules are used.
69: //--------------------------------------------------------------------------------------------------------------------------------------
70: //Eigen Uses
71: // 0 - X = unknown
72: // > X = Recognised
73: //
74: //Fisher and LBPH Use
75: // 0 - X = Recognised
76: // > X = Unknown
77: //
78: // Where X = Threshold value
79:
80:
81: switch (Recognizer_Type)
82: {
83: case ("EMGU.CV.LBPHFaceRecognizer"):
84: recognizer = new LBPHFaceRecognizer(1, 8, 8, 8, 100);//50
85: break;
86: case ("EMGU.CV.FisherFaceRecognizer"):
87: recognizer = new FisherFaceRecognizer(0, 3500);//4000
88: break;
89: case("EMGU.CV.EigenFaceRecognizer"):
90: default:
91: recognizer = new EigenFaceRecognizer(80, double.PositiveInfinity);
92: break;
93: }
94:
95: recognizer.Train(trainingImages.ToArray(), Names_List_ID.ToArray());
96: // Recognizer_Type = recognizer.GetType();
97: // string v = recognizer.ToString(); //EMGU.CV.FisherFaceRecognizer || EMGU.CV.EigenFaceRecognizer || EMGU.CV.LBPHFaceRecognizer
98:
99: return true;
100: }
101: else return false;
102: }
103: catch (Exception ex)
104: {
105: Error = ex.ToString();
106: return false;
107: }
108: }
109: else return false;
110: }
=======================================================
UI外觀如下
加入參考Emgu.CV Emgu.CV.UI Emgu.Util
C:\Emgu\libemgucv-windows-universal-cuda-2.4.10.1940\bin\ 支援FaceRecognizer
C:\Emgu\emgucv-windows-universal-cuda 3.0.0.2131\bin\ 不支援FaceRecognizer
1: using Emgu.CV.UI;
2: using Emgu.CV;
3: using Emgu.CV.Structure;
4: using Emgu.CV.CvEnum;
5:
6: using System.IO;
7: using System.Xml;
8: using System.Runtime.InteropServices;
9: using System.Threading;
10: using System.Security.Principal;
11: using System.Threading.Tasks;
12: using Microsoft.Win32.SafeHandles;
在專案中新增一個資料夾Cascade
複製C:\Emgu\emgucv-windows-universal-cuda 3.0.0.2131\bin\haarcascade_frontalface_default.xml
到Cascade目錄下
專案新增x64目錄並加入C:\Emgu\emgucv-windows-universal-cuda 2.4.10.1940\bin\x64目錄下所有dll檔案
加入Classifier_Train.cs至專案
Variable段落
1: #region variables
2: Image<Bgr, Byte> currentFrame; //current image aquired from webcam for display
3: Image<Gray, byte> result, TrainedFace = null; //used to store the result image and trained face
4: Image<Gray, byte> gray_frame = null; //grayscale current image aquired from webcam for processing
5:
6: Capture grabber; //This is our capture variable
7:
8: public CascadeClassifier Face = new CascadeClassifier(Application.StartupPath + "/Cascades/haarcascade_frontalface_default.xml");//Our face detection method
9:
10: MCvFont font = new MCvFont(FONT.CV_FONT_HERSHEY_COMPLEX, 0.5, 0.5); //Our fount for writing within the frame
11:
12: int NumLabels;
13:
14: //Classifier with default training location
15: Classifier_Train Eigen_Recog = new Classifier_Train();
16:
17: #endregion
定義兩個攝影機影像擷取事件: FrameGrabber_Standard和FrameGrabber_Parrellel
1: void FrameGrabber_Standard(object sender, EventArgs e)
2: {
3: }
4: void FrameGrabber_Parrellel(object sender, EventArgs e)
5: {
6: }
先來看FrameGrabber_Standard
人臉框選會往內縮一定比例, 以去除背景雜訊(非人臉)
1: //Camera Start Stop
2: void FrameGrabber_Standard(object sender, EventArgs e)
3: {
4: //Get the current frame form capture device
5: currentFrame = grabber.QueryFrame().Resize(320, 240, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);
6: double scaleFactor = 1.2;
7: int minNeighbors = 10;
8: Size minSize = new Size(30,30);
9: Size maxSize = new Size(130, 130);
10: double scale_x = 0.15, scale_y = 0.12, scale_h = 0.15, scale_w = 0.2; // 人臉矩形縮放比例
11: int normal_wid = 100; // 正規化寬度
12: int normal_hei = 100;
13: if (currentFrame != null)
14: {
15: gray_frame = currentFrame.Convert<Gray, byte>();
16:
17: //Face Detector
18: //Rectangle[] facesDetected = Face.DetectMultiScale(gray_frame, 1.2, 10, new Size(50, 50), Size.Empty);
19: Rectangle[] facesDetected = Face.DetectMultiScale(gray_frame, scaleFactor, minNeighbors, minSize, maxSize);
20: //Action for each element detected
21: for (int i = 0; i < facesDetected.Length; i++)// (Rectangle face_found in facesDetected)
22: {
23: //This will focus in on the face from the haar results its not perfect but it will remove a majoriy
24: //of the background noise
25: facesDetected[i].X += (int)(facesDetected[i].Height * scale_x);
26: facesDetected[i].Y += (int)(facesDetected[i].Width * scale_y);
27: facesDetected[i].Height -= (int)(facesDetected[i].Height * scale_h);
28: facesDetected[i].Width -= (int)(facesDetected[i].Width * scale_w);
29:
30: result = currentFrame.Copy(facesDetected[i]).Convert<Gray, byte>().Resize(normal_wid, normal_hei, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);
31: result._EqualizeHist();
32: //draw the face detected in the 0th (gray) channel with blue color
33: currentFrame.Draw(facesDetected[i], new Bgr(Color.Blue), 2);
34: currentFrame.Draw((i+1).ToString() + " ", ref font2, new Point(facesDetected[i].X + 10, facesDetected[i].Y - 2), new Bgr(Color.Yellow));
35: if (Eigen_Recog.IsTrained)
36: {
37: string name = Eigen_Recog.Recognise(result);
38: int match_value = (int)Eigen_Recog.Get_Eigen_Distance;
39:
40: //Draw the label for each face detected and recognized
41: currentFrame.Draw(name + " ", ref font, new Point(facesDetected[i].X - 2, facesDetected[i].Y - 2), new Bgr(Color.LightGreen));
42: ADD_Face_Found(result, name, match_value);
43: }
44: }
45: //Show the faces procesed and recognized
46: image_PICBX.Image = currentFrame.ToBitmap();
47: }
48:
49: }