介紹
人臉識別是什么?或識別是什么?當你看到一個蘋果時,你的大腦會立刻告訴你這是一個蘋果。在這個過程中,你的大腦告訴你這是一個蘋果水果,用簡單的語言來說就是識別。那么什么是人臉識別呢?我肯定你猜對了。當你看着你的朋友走在街上或他的照片時,你會認出他是你的朋友保羅。有趣的是,當你看你的朋友或他的照片時,你首先要看他的臉,然后再看其他東西。你想過為什么要這么做嗎?這是為了讓你看他的臉就能認出他來。好吧,這是你的面部識別。
但真正的問題是人臉識別是如何工作的?它非常簡單和直觀。舉一個現實生活中的例子,當你在生活中第一次遇見一個人,你不認識他,對吧?當他和你說話或握手時,你看着他的臉、眼睛、鼻子、嘴巴、顏色和整體的表情。這是你通過收集面部數據來學習或訓練那個人的面部識別。然后他告訴你他叫保羅。此時,你的大腦知道它剛剛學到的面部數據屬於保羅。現在你的頭腦已經訓練好了,准備在保羅的臉上做面部識別。下次當你在照片中看到保羅或他的臉時,你會立刻認出他。這就是人臉識別的工作原理。你遇到保羅的次數越多,你的大腦就會收集到更多關於他的信息,尤其是他的臉,你就越能識別他。
下一個問題是如何用OpenCV編碼人臉識別,畢竟這是你閱讀這篇文章的唯一原因,對吧?那么,好吧。你可能會說我們的大腦可以很容易地做這些事情,但是把它們編碼到電腦里是很困難的嗎?別擔心,不是的。多虧了OpenCV,編碼人臉識別變得越來越容易。人臉識別的編碼步驟與我們在上面的實際示例中討論的一樣。
·訓練數據收集:收集您想要識別的人的面部數據(本例中為面部圖像)
·識別器的訓練:將人臉數據(以及每個人臉的相應名稱)輸入人臉識別器,使其能夠學習。
·識別:輸入這些人的新面孔,看看你剛訓練過的人臉識別器是否識別他們
OpenCV帶有內置的人臉識別器,你所要做的就是給它輸入人臉數據。這很簡單,一旦我們完成了編碼,它就會看起來很簡單。

OpenCV面部識別器
OpenCV有三個內置的人臉識別器,多虧了OpenCV干凈的編碼,你可以通過改變一行代碼來使用它們中的任何一個。下面是這些人臉識別器的名稱和它們的OpenCV調用
1、EigenFaces人臉識別器識別器 - cv2.face.createEigenFaceRecognizer()
2、FisherFaces人臉識別器識別器 - cv2.face.createFisherFaceRecognizer()
3、局部二值模式直方圖(LBPH)人臉識別器 - cv2.face.createLBPHFaceRecognizer()
現在我們有三個人臉識別器,但是你知道該用哪一個嗎?什么時候用嗎?或者哪個更好?我猜你不知道。那么,接下來我們將深入研究每一個識別器。
EigenFaces面部識別器
這個算法考慮的事實是,並不是臉的所有部分都同樣重要,或同樣有用。當你看一個人的時候,你會通過他獨特的特征認出他/她,比如眼睛、鼻子、臉頰、前額以及他們之間的差異。所以你實際上關注的是最大變化的區域(數學上說,這個變化是方差)。例如,從眼睛到鼻子有一個顯著的變化,從鼻子到嘴也是如此。當你看多張臉的時候你可以通過看臉的這些部分來比較它們因為這些部分是臉最有用和最重要的組成部分。重要的是,它們捕捉到人臉之間的最大變化,這種變化可以幫助你區分不同的人臉,這就是特征人臉識別系統的工作原理。
EignFaces人臉識別器將所有人的訓練圖像作為一個整體,並試圖提取重要和有用的成分(捕捉最大方差/變化的成分),並丟棄其余的成分。這樣,它不僅從訓練數據中提取重要的組件,而且通過丟棄不重要的組件來節省內存。它提取的這些重要成分被稱為主成分。
我所用主成分,方差,高變化區域,可互換的有用特征等術語,它們的性質基本上是一樣的東西。
以下是顯示從面部列表中提取的主要組件的圖像。
主成分

你可以看到,主分量實際上表示面的,這些面被稱為特征面,也就是算法的名字。
這就是特征面識別器自身的訓練方式(通過提取主成分),它還記錄了哪個主成分屬於哪個人。在上面的圖像中需要注意的一點是特征面算法也將光照作為一個重要的組成部分。
在隨后的識別過程中,當你向算法輸入新圖像時,它也會在該圖像上重復同樣的過程。它從新映像中提取主組件,並將該組件與它在訓練期間存儲的組件列表進行比較,並找到匹配最好的組件,並返回與該最佳匹配組件關聯的person標簽。
輕松+容易,對吧? 下一個比這個更容易。
FisherFaces人臉識別器
該算法是改進后的FisherFaces人臉識別算法。FisherFaces人臉識別器同時查看所有人的訓練面,並從所有人的訓練面中找到主要的組成部分。通過從所有的人臉中捕獲主要的組成部分,你並沒有把注意力集中在區分一個人和另一個人的特征上,而是集中在代表整個訓練數據中所有人的所有面孔的特征上。
這種方法有一個缺點。 例如,考慮下面的面光照變化。

你知道特征面人臉識別器也認為照明是一個重要的組成部分,對吧?想象一個場景,一個人所有的臉都有非常高的亮度變化(非常暗或者非常亮等等)。特征人臉識別者將會考慮這些光照變化非常有用的特征,並且可能會忽略其他人的面部特征,認為這些特征不太有用。現在所提取的特征特征面只代表一個人的面部特征,而不是所有人的面部特征。
如何解決這個問題? 我們可以通過調整EigenFaces人臉識別器來解決這個問題,以便從每個人的臉部分別提取有用的特征,而不是提取所有臉部組合的有用特征。 這樣,即使一個人的光照變化很大,也不會影響其他人物特征提取過程。 這正是FisherFaces人臉識別器算法的功能。
Fisherfaces算法不是提取表示所有人員所有面部的有用特征,而是提取可區分一個人和另一個人的有用特征。 通過這種方式,一個人的特征不會占據主導地位(被認為是更有用的特征)而其他人則具有區分一個人和另一個人的特征。
下面是使用Fisherfaces算法提取的特征的圖像。
Fisher Faces

你可以看到提取的特征實際上代表了面孔,這些面被稱為Fisher faces,因此算法的名稱。
這里需要注意的一點是,Fisherfaces人臉識別器只會阻止一個人的特征凌駕於另一個人的特征之上,但它仍然認為光照變化是有用的特征。我們知道光照變化不是一個有用的特征來提取,因為它不是真正的臉的一部分。那么,該怎么擺脫這個照明問題?這就是我們的下一個人臉識別器鎖解決的問題。
局部二值模式直方圖(LBPH)人臉識別器
我們知道Eigenfaces和Fisherfaces都受光線影響,在現實生活中,我們無法保證完美的光照條件。 LBPH人臉識別器是克服這個缺點的一種改進。
這種想法是不看整個圖像,而是查找圖像的局部特征。 LBPH算法試圖找出圖像的局部結構,並通過比較每個像素與其相鄰像素來實現。
取一個3x3的窗口,每移動一個圖像(圖像的每個局部),將中心的像素與相鄰像素進行比較。強度值小於或等於中心像素的鄰域用1表示,其它鄰域用0表示。然后你以順時針的順序讀取3x3窗口下的0/1值,你會得到一個像11100011這樣的二進制模式,這個模式在圖像的特定區域是局部的。在整個圖像上這樣做,就會得到一個局部二進制模式的列表。
LBP標簽

現在你明白為什么這個算法的名字中有局部二進制模式? 因為你得到一個局部二進制模式列表。 現在你可能想知道,LBPH的直方圖部分呢? 在獲得局部二進制模式列表后,您可以使用二進制到十進制轉換將每個二進制模式轉換為十進制數(如上圖所示),然后對所有這些十進制值進行直方圖制作。 樣本直方圖是像下面這樣的。
樣本直方圖

我猜這回答了直方圖部分的問題。所以最終你會得到訓練數據集中每個人臉圖像的一個直方圖,這意味着如果訓練數據集中有100個圖像,那么LBPH會在訓練后提取100個直方圖,並儲存起來以便以后識別。記住,算法也會跟蹤哪個直方圖屬於哪個人。
在識別后期,當您將新圖像送入識別器進行識別時,它將生成新圖像的直方圖,將該直方圖與其已有的直方圖進行比較,找到最佳匹配直方圖並返回與該最佳匹配關聯的人員標簽 匹配直方圖。
下面是一張臉和它們各自的局部二進制模式圖像的列表。您可以看到,LBP圖像不受光照條件變化的影響。
局部人臉

理論部分已經結束,現在是編碼部分!准備好開始編寫代碼了嗎?那我們開始吧。
使用OpenCV編碼人臉識別
本教程中的人臉識別過程分為三個步驟。
1、准備訓練數據:在這一步中,我們將讀取每個人/主體的訓練圖像及其標簽,從每個圖像中檢測人臉並為每個檢測到的人臉分配其所屬人員的整數標簽。
2、訓練人臉識別器:在這一步中,我們將訓練OpenCV的LBPH人臉識別器,為其提供我們在步驟1中准備的數據。
3、測試:在這一步中,我們會將一些測試圖像傳遞給人臉識別器,並查看它是否能夠正確預測它們
編程工具:
注:Numpy使Python中的計算變得容易。 除此之外,它還包含一個強大的N維數組實現,我們將使用它來將數據作為OpenCV函數的輸入。
導入必需的模塊
在開始實際編碼之前,我們需要導入所需的編碼模塊。 所以讓我們先導入它們。
cv2:是Python的OpenCV模塊,我們將用它來進行人臉檢測和人臉識別。
os:我們將使用這個Python模塊來讀取我們的培訓目錄和文件名。
numpy:我們將使用此模塊將Python列表轉換為numpy數組,因為OpenCV人臉識別器接受numpy數組。
#導入OpenCV模塊 import cv2 #導入os模塊用於讀取訓練數據目錄和路徑 import os # 導入numpy將python列表轉換為numpy數組,OpenCV面部識別器需要它 import numpy as np
訓練數據
訓練中使用的圖像越多越好。 通常很多圖像用於訓練面部識別器,以便它可以學習同一個人的不同外觀,例如戴眼鏡,不戴眼鏡,笑,傷心,快樂,哭泣,留着胡子,沒有胡子等。 簡單的教程我們將只為每個人使用12張圖片。
所以我們的訓練數據由共2人組成,每個人有12張圖像。 所有培訓數據都在培訓數據文件夾內。 訓練數據文件夾包含每個人的一個文件夾,並且每個文件夾以格式sLabel(例如s1,s2)命名,其中標簽實際上是分配給該人的整數標簽。 例如,名為s1的文件夾意味着該文件夾包含人員1的圖像。培訓數據的目錄結構樹如下所示:
training-data
|-------------- s1
| |-- 1.jpg
| |-- ...
| |-- 12.jpg
|-------------- s2
| |-- 1.jpg
| |-- ...
| |-- 12.jpg
測試數據文件夾包含我們將用於在成功培訓完成后測試人臉識別器的圖像
由於OpenCV人臉識別器接受標簽為整數,因此我們需要定義整數標簽和人物實際名稱之間的映射,所以下面我定義了人員整數標簽及其各自名稱的映射。
注意:由於我們尚未將標簽0分配給任何人,因此標簽0的映射為空。
#我們的訓練數據中沒有標簽0,因此索引/標簽0的主題名稱為空 subjects = ["", "Ramiz Raja", "Elvis Presley"]
准備訓練數據
您可能想知道為什么要進行數據准備,對嗎? 那么,OpenCV人臉識別器接受特定格式的數據。 它接受兩個矢量,一個矢量是所有人的臉部,第二個矢量是每個臉部的整數標簽,因此在處理臉部時,臉部識別器會知道該臉部屬於哪個人。
例如,如果我們有兩個人和兩個圖像為每個人。
PERSON-1 PERSON-2
img1 img1
img2 img2
然后,准備數據步驟將生成以下面和標簽向量。
FACES LABELS
person1_img1_face 1
person1_img2_face 1
person2_img1_face 2
person2_img2_face 2
准備數據步驟可以進一步分為以下子步驟。
1、閱讀培訓數據文件夾中提供的所有主題/人員的文件夾名稱。 例如,在本教程中,我們有文件夾名稱:s1,s2。
2、對於每個主題,提取標簽號碼。 你還記得我們的文件夾有一個特殊的命名約定嗎? 文件夾名稱遵循格式sLabel,其中Label是一個整數,代表我們已分配給該主題的標簽。 因此,例如,文件夾名稱s1表示主題具有標簽1,s2表示主題標簽為2等。 將在此步驟中提取的標簽分配給在下一步中檢測到的每個面部。
3、閱讀主題的所有圖像,從每張圖像中檢測臉部。
4、將添加到標簽矢量中的具有相應主題標簽(在上述步驟中提取)的每個臉部添加到臉部矢量。
#使用OpenCV用來檢測臉部的函數 def detect_face(img): #將測試圖像轉換為灰度圖像,因為opencv人臉檢測器需要灰度圖像 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) #加載OpenCV人臉檢測器,我正在使用的是快速的LBP #還有一個更准確但緩慢的Haar分類器 face_cascade = cv2.CascadeClassifier('opencv-files/lbpcascade_frontalface.xml') #讓我們檢測多尺度(一些圖像可能比其他圖像更接近相機)圖像 #結果是一張臉的列表 faces = face_cascade.detectMultiScale(gray, scaleFactor=1.2, minNeighbors=5); #如果未檢測到面部,則返回原始圖像 if (len(faces) == 0): return None, None #假設只有一張臉, #提取面部區域 (x, y, w, h) = faces[0] #只返回圖像的正面部分 return gray[y:y+w, x:x+h], faces[0]
我正在使用OpenCV的LBP人臉檢測器。 在第4行,我將圖像轉換為灰度,因為OpenCV中的大多數操作都是以灰度進行的,然后在第8行使用cv2.CascadeClassifier類加載LBP人臉檢測器。 在第12行之后,我使用cv2.CascadeClassifier類'detectMultiScale方法來檢測圖像中的所有面部。 在第20行中,從檢測到的臉部我只挑選第一張臉部,因為在一張圖像中只有一張臉部(假設只有一張醒目的臉部)。 由於detectMultiScale方法返回的面實際上是矩形(x,y,寬度,高度),而不是實際的面部圖像,所以我們必須從主圖像中提取面部圖像區域。 所以在第23行我從灰色圖像中提取人臉區域並返回人臉圖像區域和人臉矩形。
現在您已經有了一個面部檢測器,您知道准備數據的4個步驟,那么您准備好編寫准備數據步驟了嗎?是嗎?讓我們來做它。
#該功能將讀取所有人的訓練圖像,從每個圖像檢測人臉 #並將返回兩個完全相同大小的列表,一個列表 # 每張臉的臉部和另一列標簽 def prepare_training_data(data_folder_path): #------STEP-1-------- #獲取數據文件夾中的目錄(每個主題的一個目錄) dirs = os.listdir(data_folder_path) #列表來保存所有主題的面孔 faces = [] #列表以保存所有主題的標簽 labels = [] #讓我們瀏覽每個目錄並閱讀其中的圖像 for dir_name in dirs: #我們的主題目錄以字母's'開頭 #如果有的話,忽略任何不相關的目錄 if not dir_name.startswith("s"): continue; #------STEP-2-------- #從dir_name中提取主題的標簽號 #目錄名稱格式= slabel #,所以從dir_name中刪除字母''會給我們標簽 label = int(dir_name.replace("s", "")) #建立包含當前主題主題圖像的目錄路徑 #sample subject_dir_path = "training-data/s1" subject_dir_path = data_folder_path + "/" + dir_name #獲取給定主題目錄內的圖像名稱 subject_images_names = os.listdir(subject_dir_path) #------STEP-3-------- #瀏覽每個圖片的名稱,閱讀圖片, #檢測臉部並將臉部添加到臉部列表 for image_name in subject_images_names: #忽略.DS_Store之類的系統文件 if image_name.startswith("."): continue; #建立圖像路徑 #sample image path = training-data/s1/1.pgm image_path = subject_dir_path + "/" + image_name #閱讀圖像 image = cv2.imread(image_path) #顯示圖像窗口以顯示圖像 cv2.imshow("Training on image...", image) cv2.waitKey(100) #偵測臉部 face, rect = detect_face(image) #------STEP-4-------- #為了本教程的目的 #我們將忽略未檢測到的臉部 if face is not None: #將臉添加到臉部列表 faces.append(face) #為這張臉添加標簽 labels.append(label) cv2.destroyAllWindows() cv2.waitKey(1) cv2.destroyAllWindows() return faces, labels
我已經定義了一個函數,它將存儲培訓主題文件夾的路徑作為參數。 該功能遵循上述的4個准備數據子步驟。
(step--1)在第8行,我使用os.listdir方法讀取存儲在傳遞給函數的路徑上的所有文件夾的名稱作為參數。 在第10-13行,我定義了標簽並面向矢量。
(step--2)之后,我遍歷所有主題的文件夾名稱以及第27行中每個主題的文件夾名稱,我將提取標簽信息。 由於文件夾名稱遵循sLabel命名約定,所以從文件夾名稱中刪除字母將給我們分配給該主題的標簽。
(step--3)在第34行,我讀取了當前被攝體的所有圖像名稱,並且在第39-66行中我逐一瀏覽了這些圖像。 在53-54行,我使用OpenCV的imshow(window_title,image)和OpenCV的waitKey(interval)方法來顯示當前正在傳播的圖像。 waitKey(interval)方法將代碼流暫停給定的時間間隔(毫秒),我以100ms的間隔使用它,以便我們可以查看100ms的圖像窗口。 在第57行,我從當前正在遍歷的圖像中檢測出臉部。
(step--4)在第62-66行,我將檢測到的面和標簽添加到它們各自的向量中。
但是一個函數只能在需要准備的某些數據上調用它時才能做任何事情,對嗎? 別擔心,我有兩張臉的數據。 我相信你至少會認出其中的一個!

讓我們在這些美麗的名人的圖像上調用這個函數來准備數據來訓練我們的人臉識別器。 下面是一個簡單的代碼來做到這一點。
#讓我們先准備好我們的訓練數據 #數據將在兩個相同大小的列表中 #一個列表將包含所有的面孔 #數據將在兩個相同大小的列表中 print("Preparing data...") faces, labels = prepare_training_data("training-data") print("Data prepared") #打印總面和標簽 print("Total faces: ", len(faces)) print("Total labels: ", len(labels))
訓練人臉識別器
我們知道,OpenCV配備了三個人臉識別器。
1、EigenFaces人臉識別器識別器 - cv2.face.createEigenFaceRecognizer()
2、FisherFaces人臉識別器識別器 - cv2.face.createFisherFaceRecognizer()
3、局部二值模式直方圖(LBPH)人臉識別器 - cv2.face.createLBPHFaceRecognizer()
我將使用LBPH人臉識別器,但您可以使用您選擇的任何人臉識別器。 無論您使用哪個OpenCV的臉部識別器,其代碼都將保持不變。 您只需更改一行,即下面給出的面部識別器初始化行。
#創建我們的LBPH人臉識別器 face_recognizer = cv2.face.createLBPHFaceRecognizer() #或者使用EigenFaceRecognizer替換上面的行 #face_recognizer = cv2.face.createEigenFaceRecognizer() #或者使用FisherFaceRecognizer替換上面的行 #face_recognizer = cv2.face.createFisherFaceRecognizer()
現在我們已經初始化了我們的人臉識別器,也准備了我們的訓練數據,現在是時候訓練人臉識別器了。我們將通過調用人臉識別器的序列(面向量,標簽向量)方法來實現這一點。
#訓練我們的面部識別器 face_recognizer.train(faces, np.array(labels))
你有沒有注意到,不是直接將標簽矢量直接傳遞給人臉識別器,而是先把它轉換成numpy數組?這是因為OpenCV希望標簽向量是一個numpy數組。
仍然不滿意? 想看到一些行動? 下一步是真正的行動,我保證!
預測
現在是我最喜歡的部分,預測部分。 這是我們真正了解我們的算法是否確實能夠識別受過訓練的對象臉部的地方。 我們將拍攝兩張我們的景點的測試圖像,從他們每個人身上檢測臉部,然后將這些臉部傳遞給我們訓練有素的臉部識別器,看看它們是否識別它們。
下面是一些實用功能,我們將用它來繪制圍繞臉部的邊界框(矩形)並將邊界名稱放在邊界框附近。
#函數在圖像上繪制矩形 #根據給定的(x,y)坐標和 #給定的寬度和高度 def draw_rectangle(img, rect): (x, y, w, h) = rect cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2) #函數在從圖像開始繪制文本 #通過(x,y)坐標。 def draw_text(img, text, x, y): cv2.putText(img, text, (x, y), cv2.FONT_HERSHEY_PLAIN, 1.5, (0, 255, 0), 2)
第一個函數draw_rectangle根據傳入的矩形坐標在圖像上繪制一個矩形。 它使用OpenCV的內置函數cv2.rectangle(img,topLeftPoint,bottomRightPoint,rgbColor,lineWidth)繪制矩形。 我們將使用它在測試圖像中檢測到的臉部周圍畫一個矩形。
第二個函數draw_text使用OpenCV的內置函數cv2.putText(img,text,startPoint,font,fontSize,rgbColor,lineWidth)在圖像上繪制文本。
既然我們有繪圖功能,我們只需要調用人臉識別器預測(人臉)方法來測試我們的測試圖像上的人臉識別器。 以下功能為我們做了預測。
#this function recognizes the person in image passed #and draws a rectangle around detected face with name of the #學科 def predict(test_img): #制作圖像的副本,因為我們不想更改原始圖像 img = test_img.copy() #從圖像中檢測臉部 face, rect = detect_face(img) #使用我們的臉部識別器預測圖像 label= face_recognizer.predict(face) #獲取由人臉識別器返回的相應標簽的名稱 label_text = subjects[label] #在檢測到的臉部周圍畫一個矩形 draw_rectangle(img, rect) #畫預計人的名字 draw_text(img, label_text, rect[0], rect[1]-5) return img
第6行讀取測試圖像
第7行從測試圖像中檢測臉部
第11行通過調用面部識別器的預測(面部)方法來識別面部。 該方法將返回一個標簽
第12行獲取與標簽關聯的名稱
第16行在檢測到的臉部周圍繪制矩形
第18行繪制預測主體在面部矩形上方的名稱
現在我們已經很好地定義了預測函數,下一步就是在我們的測試圖像上實際調用這個函數,並顯示這些測試圖像以查看我們的人臉識別器是否能正確識別它們。 所以讓我們來做。 這就是我們一直在等待的。
print("Predicting images...") #加載測試圖像 test_img1 = cv2.imread("test-data/test1.jpg") test_img2 = cv2.imread("test-data/test2.jpg") #執行預測 predicted_img1 = predict(test_img1) predicted_img2 = predict(test_img2) print("Prediction complete") #顯示兩個圖像 cv2.imshow(subjects[1], predicted_img1) cv2.imshow(subjects[2], predicted_img2) cv2.waitKey(0) cv2.destroyAllWindows()
Predicting images... Prediction complete

結語
你可以從這個Github下載完整的代碼和相關文件 打開GitHab.
人臉識別是一個非常有趣的想法,OpenCV使得它非常簡單,易於我們對其進行編碼。 只需幾行代碼即可完成全面工作的人臉識別應用程序,並且我們可以通過一行代碼更改在所有三個人臉識別器之間切換。 就這么簡單。
盡管EigenFaces,FisherFaces和LBPH人臉識別器都不錯,但是使用面向梯度直方圖(HOG)和神經網絡進行人臉識別還有更好的方法。 因此,更先進的人臉識別算法現在是一個使用OpenCV和機器學習相結合的日子。 我還計划寫一些關於這些更高級方法的文章,敬請關注!
