人臉檢測是OpenCV的一個很不錯的功能,它是人臉識別的基礎。什么是人臉識別?其實就是一個程序能夠識別出給定圖像或者視頻中的人臉。實現這一目標的方法之一是用一系列分好類的圖像(人臉數據庫)來訓練,並基於這些圖像進行識別。
人臉識別所需要的人臉庫可以通過兩種方式來獲得:自己獲得圖像或從人臉數據庫免費獲得可用的人臉圖像,互聯網上有許多人臉數據庫,這里以ORL人臉庫(包含40個人,每人10張人臉,共400張人臉)為例,ORL人臉庫中每一張圖像大小為92x112,我們要想對這些樣本進行人臉識別,必須要在包含人臉的樣本圖像上進行人臉識別。除了要識別ORL人臉庫,我們也還想要識別出自己,所以我們還需要准備自己的圖像。
一 生成自己的人臉圖像
我們通過攝像頭采集自己的人臉,大約10張圖像就可以,我們需要把圖像調整為92x112的大小,並且保存到一個指定文件夾,文件名后綴為.pgm。代碼如下:
#1、生成自己人臉識別數據 def generator(data): ''' 生成的圖片滿足以下條件 1、圖像是灰度格式,后綴為.pgm 2、圖像大小要一樣 params: data:指定生成的人臉數據的保存路徑 ''' ''' 打開攝像頭,讀取幀,檢測幀中的人臉,並剪切,縮放 ''' name = input('my name:') #如果路徑存在則刪除 path = os.path.join(data,name) if os.path.isdir(path): #os.remove(path) #刪除文件 #os.removedirs(path) #刪除空文件夾 shutil.rmtree(path) #遞歸刪除文件夾 #創建文件夾 os.mkdir(path) #創建一個級聯分類器 加載一個 .xml 分類器文件. 它既可以是Haar特征也可以是LBP特征的分類器. face_cascade = cv2.CascadeClassifier('./haarcascades/haarcascade_frontalface_default.xml') #打開攝像頭 camera = cv2.VideoCapture(0) cv2.namedWindow('Dynamic') #計數 count = 1 while(True): #讀取一幀圖像 ret,frame = camera.read() #判斷圖片讀取成功? if ret: gray_img = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY) #人臉檢測 faces = face_cascade.detectMultiScale(gray_img,1.3,5) for (x,y,w,h) in faces: #在原圖像上繪制矩形 cv2.rectangle(frame,(x,y),(x+w,y+h),(255,0,0),2) #調整圖像大小 和ORL人臉庫圖像一樣大小 f = cv2.resize(frame[y:y+h,x:x+w],(92,112)) #保存人臉 cv2.imwrite('%s/%s.pgm'%(path,str(count)),f) count += 1 cv2.imshow('Dynamic',frame) #如果按下q鍵則退出 if cv2.waitKey(100) & 0xff == ord('q') : break camera.release() cv2.destroyAllWindows()
程序運行后,我們需要輸入自己的姓名,這里我輸入zy,並在data路徑下生成了一個zy文件夾,下面保存着采集到的圖像:
二 人臉識別(OpenCV)
OpenCV 3有三種人臉識別的方法,它們分別基於不同的三種算法,Eigenfaces,Fisherfaces和Local Binary Pattern Histogram。
這些方法都有一個類似的過程,即都使用分好類的訓練數據集來進行訓練,對圖像或視頻中檢測到的人臉進行分析,並從兩方面來確定:是否識別到目標;目標真正被識別到的置信度的衡量,這也稱為置信度評分,在實際應用中可以通過設置閾值來進行篩選,置信度高於該閾值的人臉將會被丟棄。
這里我們主要來介紹一下利用特征臉進行人臉識別的方法,特征臉法,本質上其實就是PCA降維,這種算法的基本思路是,把二維的圖像先灰度化,轉化為一通道的圖像,之后再把它首尾相接轉化為一個列向量,假設圖像大小是20*20的,那么這個向量就是400維,理論上講組織成一個向量,就可以應用任何機器學習算法了,但是維度太高算法復雜度也會隨之升高,所以需要使用PCA算法降維,然后使用簡單排序或者KNN都可以。
1、准備數據
我們先來准備訓練所需要的數據,這里我們需要的數據有訓練的圖像,每個圖像對應的標簽以及標簽對應的真實姓名。
#2、讀取ORL人臉數據庫 准備訓練數據 def LoadImages(data): ''' 加載數據集 params: data:訓練集數據所在的目錄,要求數據尺寸大小一樣 ret: images:[m,height,width] m為樣本數,height為高,width為寬 names:名字的集合 labels:標簽 ''' images = [] labels = [] names = [] label = 0 #過濾所有的文件夾 for subDirname in os.listdir(data): subjectPath = os.path.join(data,subDirname) if os.path.isdir(subjectPath): #每一個文件夾下存放着一個人的照片 names.append(subDirname) for fileName in os.listdir(subjectPath): imgPath = os.path.join(subjectPath,fileName) img = cv2.imread(imgPath,cv2.IMREAD_GRAYSCALE) images.append(img) labels.append(label) label += 1 images = np.asarray(images) labels = np.asarray(labels) return images,labels,names
2、人臉識別
有了訓練數據之后,我們就可以直接調用OpenCV 3的人臉識別庫進行訓練,訓練好之后,就可以進行識別:
def FaceRec(data): #加載訓練數據 X,y,names=LoadImages('./face') model = cv2.face.EigenFaceRecognizer_create() model.train(X,y) #創建一個級聯分類器 加載一個 .xml 分類器文件. 它既可以是Haar特征也可以是LBP特征的分類器. face_cascade = cv2.CascadeClassifier('./haarcascades/haarcascade_frontalface_default.xml') #打開攝像頭 camera = cv2.VideoCapture(0) cv2.namedWindow('Dynamic') while(True): #讀取一幀圖像 ret,frame = camera.read() #判斷圖片讀取成功? if ret: gray_img = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY) #人臉檢測 faces = face_cascade.detectMultiScale(gray_img,1.3,5) for (x,y,w,h) in faces: #在原圖像上繪制矩形 frame = cv2.rectangle(frame,(x,y),(x+w,y+h),(255,0,0),2) roi_gray = gray_img[y:y+h,x:x+w] try: #寬92 高112 roi_gray = cv2.resize(roi_gray,(92,112),interpolation=cv2.INTER_LINEAR) params = model.predict(roi_gray) print('Label:%s,confidence:%.2f'%(params[0],params[1])) cv2.putText(frame,names[params[0]],(x,y-20),cv2.FONT_HERSHEY_SIMPLEX,1,255,2) except: continue cv2.imshow('Dynamic',frame) #如果按下q鍵則退出 if cv2.waitKey(100) & 0xff == ord('q') : break camera.release() cv2.destroyAllWindows()
這里我們cv2.face.EigenFaceRecognizer_create()創建人臉識別模型,通過圖像數組和對應標簽數組來訓練模型,EigenFaceRecognizer_create()函數有兩個可以設置的重要參數:第一個是想要保留的主成分數目,第二個是指定的置信度閾值,這是一個浮點數。
接下來,重復與人臉檢測操作類似的過程。通過在檢測到的人臉上進行人臉識別,注意這里有兩個步驟:
1、將檢測到人臉調整為指定的大小92x112,即與訓練集圖像尺寸一樣;
2、調用prdict()函數進行預測,該函數返回有兩個元素的數組,第一個元素是所識別個體的標簽,第二個是置信度評分,用來衡量所識別人臉與原模型的差距,0表示完全匹配。
注意:Eigenfaces/Fisherfaces和LBPH的置信度評分值完全不同,Eigenfaces和Fisherfaces將產生0到20000的值,而任意低於4000到5000的評分都是相當可靠的識別。LBPH有着類似的工作方式,但是一個好的識別參考值要低於50,任意高於80的參數值都被認為是低的置信度評分。
源代碼:

# -*- coding: utf-8 -*- """ Created on Thu Aug 16 19:41:19 2018 @author: lenovo """ ''' 調用opencv庫實現人臉識別 ''' import numpy as np import cv2 import os import shutil #讀取pgm圖像,並顯示 def ShowPgm(filepath): cv2.namedWindow('pgm') img = cv2.imread(filepath) cv2.imshow('pgm',img) print(img.shape) cv2.waitKey(0) cv2.destroyAllWindows() #1、生成自己人臉識別數據 def generator(data): ''' 生成的圖片滿足以下條件 1、圖像是灰度格式,后綴為.pgm 2、圖像大小要一樣 params: data:指定生成的人臉數據的保存路徑 ''' ''' 打開攝像頭,讀取幀,檢測幀中的人臉,並剪切,縮放 ''' name = input('my name:') #如果路徑存在則刪除 path = os.path.join(data,name) if os.path.isdir(path): #os.remove(path) #刪除文件 #os.removedirs(path) #刪除空文件夾 shutil.rmtree(path) #遞歸刪除文件夾 #創建文件夾 os.mkdir(path) #創建一個級聯分類器 加載一個 .xml 分類器文件. 它既可以是Haar特征也可以是LBP特征的分類器. face_cascade = cv2.CascadeClassifier('./haarcascades/haarcascade_frontalface_default.xml') #打開攝像頭 camera = cv2.VideoCapture(0) cv2.namedWindow('Dynamic') #計數 count = 1 while(True): #讀取一幀圖像 ret,frame = camera.read() #判斷圖片讀取成功? if ret: gray_img = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY) #人臉檢測 faces = face_cascade.detectMultiScale(gray_img,1.3,5) for (x,y,w,h) in faces: #在原圖像上繪制矩形 cv2.rectangle(frame,(x,y),(x+w,y+h),(255,0,0),2) #調整圖像大小 和ORL人臉庫圖像一樣大小 f = cv2.resize(frame[y:y+h,x:x+w],(92,112)) #保存人臉 cv2.imwrite('%s/%s.pgm'%(path,str(count)),f) count += 1 cv2.imshow('Dynamic',frame) #如果按下q鍵則退出 if cv2.waitKey(100) & 0xff == ord('q') : break camera.release() cv2.destroyAllWindows() #2、讀取ORL人臉數據庫 准備訓練數據 def LoadImages(data): ''' 加載數據集 params: data:訓練集數據所在的目錄,要求數據尺寸大小一樣 ret: images:[m,height,width] m為樣本數,height為高,width為寬 names:名字的集合 labels:標簽 ''' images = [] labels = [] names = [] label = 0 #過濾所有的文件夾 for subDirname in os.listdir(data): subjectPath = os.path.join(data,subDirname) if os.path.isdir(subjectPath): #每一個文件夾下存放着一個人的照片 names.append(subDirname) for fileName in os.listdir(subjectPath): imgPath = os.path.join(subjectPath,fileName) img = cv2.imread(imgPath,cv2.IMREAD_GRAYSCALE) images.append(img) labels.append(label) label += 1 images = np.asarray(images) labels = np.asarray(labels) return images,labels,names def FaceRec(data): #加載訓練數據 X,y,names=LoadImages('./face') model = cv2.face.EigenFaceRecognizer_create() model.train(X,y) #創建一個級聯分類器 加載一個 .xml 分類器文件. 它既可以是Haar特征也可以是LBP特征的分類器. face_cascade = cv2.CascadeClassifier('./haarcascades/haarcascade_frontalface_default.xml') #打開攝像頭 camera = cv2.VideoCapture(0) cv2.namedWindow('Dynamic') while(True): #讀取一幀圖像 ret,frame = camera.read() #判斷圖片讀取成功? if ret: gray_img = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY) #人臉檢測 faces = face_cascade.detectMultiScale(gray_img,1.3,5) for (x,y,w,h) in faces: #在原圖像上繪制矩形 frame = cv2.rectangle(frame,(x,y),(x+w,y+h),(255,0,0),2) roi_gray = gray_img[y:y+h,x:x+w] try: #寬92 高112 roi_gray = cv2.resize(roi_gray,(92,112),interpolation=cv2.INTER_LINEAR) params = model.predict(roi_gray) print('Label:%s,confidence:%.2f'%(params[0],params[1])) cv2.putText(frame,names[params[0]],(x,y-20),cv2.FONT_HERSHEY_SIMPLEX,1,255,2) except: continue cv2.imshow('Dynamic',frame) #如果按下q鍵則退出 if cv2.waitKey(100) & 0xff == ord('q') : break camera.release() cv2.destroyAllWindows() if __name__=='__main__': #ShowPgm('./face/s1/1.pgm') data = './face' #生成自己的人臉數據 #generator(data) FaceRec(data)
三 自己實現特征臉
上面我們介紹了調用OpenCV實現的特征臉,但是我們有時候也想自己實現一下,並不想直接調用別人的庫。
假設我們有m個樣本,每一個樣本圖片都是$w×h$大小的,可以看做$n×1$的列向量$x_i€R^n,n=w×h$,那么訓練集$S={x_1,x_2,...,x_m}$,我們利用PCA把原始向量$x_i$從高維空間$R^n$變換到低維空間$R^k,k<<n$。
PCA算法的主要步驟:
1、先求訓練集$S$的均值向量$\bar{x}=\frac{1}{m}\sum\limits_{i=1}^{m}x_i$;
2、向量取均值$Φ_i=x_i-\bar{x}$;
3、我們定義$A=[Φ_1,Φ_2,...,Φ_m]$,協方差矩陣$C=\frac{1}{m}AA^T$;
4、特征值分解$Cu_i=λ_iu_i$;
我們知道協方差矩陣是一個高維矩陣$C€R^{n×n}$,如果直接進行特征分解,無疑是非常消耗資源也很費時的。特征臉算法做了一個非常巧妙的變換,我們先來看$A^TA$的特征值分解,因為$A^TA€R^{m×m}$,與$C$比起來,維度要小得多。我們可以得到:
$$A^TAv_i=β_iv_i$$
假設$β_1,β_2,β_3,...,β_m$是$A^TA$的特征值,$v_1,v_2,v_3,...,v_m$是特征值對應的特征向量。(考慮重根$β_i$)
左乘$A$我們可以得到:
$$AA^TAv_i=β_iAv_i$$
即:$CAv_i=β_iAv_i$
所以我們可以看到$β_1,β_2,β_3,...,β_m$也是$AA^T$的特征值,$Av_1,Av_2,Av_3,...,Av_m$是特征值對應的特征向量。但是實際上$AA^T$應該有$n$個特征值(考慮重根),以及$n$個線性無關的特征向量。
又由於$A^TA$的$m$個特征值與特征向量對應着$AA^T$的前$m$個最大的特征值以及特征值相對應的特征向量。(這句話我也不怎么明白,$A^TA$的m個特征值為什么就是$AA^T$n個特征值中最大的前m個?)
所以通過這種變換, 可以非常快速地求出$AA^T$的特征向量$u_i=Av_i,i=1,2,...,m$;
注意:采用這種求特征值的方法要求$k≤m$,也就是說降維后的維數不能大於樣本數。
5、將m個特征值從大到小排序,截取前$k$個特征值及對應的特征向量。組成變換矩陣$P=[e_1,e_2,e_3...,e_k]^T,P€R^{k×n}$,$e_i$為特征向量$e_i€R^{n×1}$,則把高維向量$Φ_i$降維到低維向量$Ω_i$的變換公式為:$Ω_i = PΦ_i,Ω_i€R^{n×1},Φ_i€R^{k×1}$,由於我們訓練集中有m個樣本,我們先將訓練集中的每個人臉圖像映射到低維空間$Ω_1,Ω_2,...,Ω_m$;
6、給定一個測試樣本$x$,先做去均值$Φ=x-\bar{x}$,然后再映射到低維空間,得到低維向量$Ω$,然后我們可以通過KNN來進行分類,通過計算$Ω$與$Ω_1,Ω_2,...,Ω_m$的距離,然后進行從小到大排序,選取前g個樣本,分別獲取每個樣本對應的類別,通過投票的方式得到測試樣本的類別。
下面為實現代碼:
# -*- coding: utf-8 -*- """ Created on Fri Aug 17 17:53:30 2018 @author: lenovo """ ''' 自己實現一個人臉識別 ''' ''' 人臉識別經典算法實現(一)——特征臉法:https://blog.csdn.net/freedom098/article/details/52088064 第一種比較經典的算法就是特征臉法,本質上其實就是PCA降維,這種算法的基本思路是,把二維的圖像先灰度化, 轉化為一通道的圖像,之后再把它首尾相接轉化為一個列向量,假設圖像大小是20*20的,那么這個向量就是400維 ,理論上講組織成一個向量,就可以應用任何機器學習算法了,但是維度太高算法復雜度也會隨之升高,所以需要 使用PCA算法降維,然后使用簡單排序或者KNN都可以。 PCA降維:將一組N維向量降到K維,PCA通過尋找K個單位正交基,使得原始數據變換到這組基后各個字段兩兩間協方差 為0,而字段的方差盡可能大 http://blog.codinglabs.org/articles/pca-tutorial.html ''' import numpy as np import cv2 import os from sklearn import neighbors def load_images(data): ''' 加載數據集 params: data:訓練集數據所在的目錄,要求數據尺寸大小一樣 ret: images:[m,height,width] m為樣本數,height為高,width為寬 names:名字的集合 labels:標簽 ''' images = [] labels = [] names = [] label = 0 #過濾所有的文件夾 for subDirname in os.listdir(data): subjectPath = os.path.join(data,subDirname) if os.path.isdir(subjectPath): #每一個文件夾下存放着一個人的照片 names.append(subDirname) for fileName in os.listdir(subjectPath): imgPath = os.path.join(subjectPath,fileName) img = cv2.imread(imgPath,cv2.IMREAD_GRAYSCALE) images.append(img) labels.append(label) label += 1 images = np.asarray(images) labels = np.asarray(labels) return images,labels,names class EigenFace(object): def __init__(self,dimNum=150,n_neighbors=3,dsize=(100,100)): ''' 構造函數:初始化參數 params: dimNum:PCA降維后的維度k n_neighbors:knn的參數n_neighbors dsize:對輸入圖像進行預處理,指定圖像預處理時縮放的尺寸 ''' self.__dimNum = dimNum self.__dsize = dsize self.__mean = 0.0 self.__knn = neighbors.KNeighborsClassifier(n_neighbors) def __pca(self,X): ''' 使用PCA對數據進行降維 params: X:源數據,形狀為[m,n] m為樣本數,n為樣本的維數 return: 降維后的訓練集數據[m,k] 和變換矩陣P = [k,n] ''' #[n,m] X = X.T #均值化矩陣 [n,] mean = np.reshape(np.mean(X,axis=1),(-1,1)) self.__mean = mean #去均值 [n,m] - [n,1] diff = X - mean ''' 求協方差矩陣 這里不去直接去求np.dot(diff,diff.T)的的特征向量和特征值,而是通過求np.dot(diff.T,diff)的特征向量和特征值變換得到 主要是因為np.dot(diff,diff.T)為[n,n] np.dot(diff.T,diff)為[m,m] 特征數遠大於樣本數,所以直接對[n,n]矩陣求特征值比較慢 ''' cov = np.dot(diff.T,diff)/diff.shape[1] ''' 計算[m,m]的協方差矩陣的特征值[m,]和特征向量[m,m] ''' eigVals,eigVects = np.linalg.eig(cov) #通過左乘diff得到[n,n]矩陣的特征向量[n,m] eigVects = np.dot(diff,eigVects) print('特征向量維度:',eigVects.shape) #對特征值進行排序 返回排序后的索引順序,從小到大排序 eigValIndex = np.argsort(eigVals) #從大到小排序 eigValIndex = eigValIndex[::-1] #取出指定個數的前k大的特征值 eigValIndex = eigValIndex[:self.__dimNum] #歸一化特征向量 eigVects = eigVects/np.linalg.norm(eigVects,axis=0) #變換矩陣[k,n] transMat = (eigVects.T)[eigValIndex,:] #計算經過變換矩陣變換后的新數據 [k,n]x[n,m] = [k,m] lowMat = np.dot(transMat,diff) #[m,k] lowMat = lowMat.T print('降維后的矩陣lowMat維度為:',lowMat.shape) return lowMat,transMat def __prepare(self,images): ''' 對圖片進行預處理,統一尺寸,直方圖均衡化(防止曝光不均衡) params: images:訓練集數據,要求為灰度圖片 [m,height,width] m為樣本數,height為高,width為寬 return: 處理之后的數據 [m,n] n = dsize[0]x dsize[1] 即特征數 ''' new_images = [] for image in images: #縮放 re_img = cv2.resize(image,self.__dsize) #直方圖均衡化 hist_img = cv2.equalizeHist(re_img) #轉換成一行數據 hist_img = np.reshape(hist_img,-1) new_images.append(hist_img) new_images = np.asarray(new_images) return new_images def fit(self,X_train,Y_train): ''' 訓練,這里使用KNN算法 params: X_train:訓練集數據,要求為灰度圖片 [m,height,width] m為樣本數,height為高,width為寬 Y_train:訓練集標簽 [m,] ''' #對圖片數據進行預處理 [M,N] X_train = self.__prepare(X_train) #對圖片數據進行降維處理 X_train_pca:[m,k] __transMat:[n,k] X_train_pca,self.__transMat = self.__pca(X_train) #開始訓練 self.__knn.fit(X_train_pca,Y_train) def predict(self,X_test): ''' 開始預測 params: X_test:測試圖片,要求為灰度圖片 [m,hight,width] return: Y_pred:[m,1] 返回預測的標簽 ''' if len(X_test.shape) == 2: X_test = np.expand_dims(X_test,axis=0) #對數據進行預處理 [m,n] X_test = self.__prepare(X_test) #計算經過變換矩陣變換后的新數據 [m,k] X_test_pca = np.dot(self.__transMat,X_test.T-self.__mean) X_test_pca = X_test_pca.T Y_pred = self.__knn.predict(X_test_pca) return Y_pred if __name__=='__main__': face = EigenFace(100) #准備訓練集數據 X_train,Y_train,names = load_images('./face') face.fit(X_train,Y_train) #創建一個級聯分類器 加載一個 .xml 分類器文件. 它既可以是Haar特征也可以是LBP特征的分類器. face_cascade = cv2.CascadeClassifier('./haarcascades/haarcascade_frontalface_default.xml') #打開攝像頭 camera = cv2.VideoCapture(0) cv2.namedWindow('Dynamic') print('開始預測') while(True): #讀取一幀圖像 ret,frame = camera.read() #判斷圖片讀取成功? if ret: gray_img = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY) #人臉檢測 faces = face_cascade.detectMultiScale(gray_img,1.3,5) for (x,y,w,h) in faces: #在原圖像上繪制矩形 frame = cv2.rectangle(frame,(x,y),(x+w,y+h),(255,0,0),2) roi_gray = gray_img[y:y+h,x:x+w] Y_pred = face.predict(roi_gray) name = names[Y_pred[0]] print('Label:%s'%(name)) cv2.putText(frame,name,(x,y-20),cv2.FONT_HERSHEY_SIMPLEX,1,255,2) cv2.imshow('Dynamic',frame) #如果按下q鍵則退出 if cv2.waitKey(100) & 0xff == ord('q') : break camera.release() cv2.destroyAllWindows()
我們在介紹原理的時候,訓練集矩陣$S$,每一列都對應一個樣本,然而在代碼實現的時候,我們傳入的訓練集是每一行對應一個樣本,所以在__pca()函數中對輸入數據做了一個轉置,同理在輸入數據降維之后輸出時我們也做了一個轉置,使得輸出的矩陣每一行對應一個樣本。
參考文獻
[4]PCA的數學原理