1. 程式人生 > >第三十七節、人臉檢測MTCNN和人臉識別Facenet(附原始碼)

第三十七節、人臉檢測MTCNN和人臉識別Facenet(附原始碼)

在說到人臉檢測我們首先會想到利用Harr特徵提取和Adaboost分類器進行人臉檢測(有興趣的可以去一看這篇部落格第九節、人臉檢測之Haar分類器),其檢測效果也是不錯的,但是目前人臉檢測的應用場景逐漸從室內演變到室外,從單一限定場景發展到廣場、車站、地鐵口等場景,人臉檢測面臨的要求越來越高,比如:人臉尺度多變、數量冗大、姿勢多樣包括俯拍人臉、戴帽子口罩等的遮擋、表情誇張、化妝偽裝、光照條件惡劣、解析度低甚至連肉眼都較難區分等。在這樣複雜的環境下基於Haar特徵的人臉檢測表現的不盡人意。隨著深度學習的發展,基於深度學習的人臉檢測技術取得了巨大的成功,在這一節我們將會介紹MTCNN演算法,它是基於卷積神經網路的一種高精度的實時人臉檢測和對齊技術。

搭建人臉識別系統的第一步就是人臉檢測,也就是在圖片中找到人臉的位置。在這個過程中輸入的是一張含有人臉的影象,輸出的是所有人臉的矩形框。一般來說,人臉檢測應該能夠檢測出影象中的所有人臉,不能有漏檢,更不能有錯檢。

獲得人臉之後,第二步我們要做的工作就是人臉對齊,由於原始影象中的人臉可能存在姿態、位置上的差異,為了之後的統一處理,我們要把人臉“擺正”。為此,需要檢測人臉中的關鍵點,比如眼睛的位置、鼻子的位置、嘴巴的位置、臉的輪廓點等。根據這些關鍵點可以使用仿射變換將人臉統一校準,以消除姿勢不同帶來的誤差。

一 MTCNN演算法結構

MTCNN演算法是一種基於深度學習的人臉檢測和人臉對齊方法,它可以同時完成人臉檢測和人臉對齊的任務,相比於傳統的演算法,它的效能更好,檢測速度更快。

MTCNN演算法包含三個子網路:Proposal Network(P-Net)、Refine Network(R-Net)、Output Network(O-Net),這三個網路對人臉的處理依次從粗到細。

在使用這三個子網路之前,需要使用影象金字塔將原始影象縮放到不同的尺度,然後將不同尺度的影象送入這三個子網路中進行訓練,目的是為了可以檢測到不同大小的人臉,從而實現多尺度目標檢測。

1、P-Net網路

P-Net的主要目的是為了生成一些候選框,我們通過使用P-Net網路,對影象金字塔影象上不同尺度下的影象的每一個$12\times{12}$區域都做一個人臉檢測(實際上在使用卷積網路實現時,一般會把一張$h\times{w}$的影象送入P-Net中,最終得到的特徵圖每一點都對應著一個大小為$12\times{12}$的感受野,但是並沒有遍歷全一張影象每一個$12\times{12}$的影象)。

P-Net的輸入是一個$12\times{12}\times{3}$的RGB影象,在訓練的時候,該網路要判斷這個$12\times{12}$的影象中是否存在人臉,並且給出人臉框的迴歸和人臉關鍵點定位;

在測試的時候輸出只有$N$個邊界框的4個座標資訊和score,當然這4個座標資訊已經使用網路的人臉框迴歸進行校正過了,score可以看做是分類的輸出(即人臉的概率):

  • 網路的第一部分輸出是用來判斷該影象是否包含人臉,輸出向量大小為$1\times{1}\times{2}$,也就是兩個值,即影象是人臉的概率和影象不是人臉的概率。這兩個值加起來嚴格等於1,之所以使用兩個值來表示,是為了方便定義交叉熵損失函式。
  • 網路的第二部分給出框的精確位置,一般稱為框迴歸。P-Net輸入的$12\times{12}$的影象塊可能並不是完美的人臉框的位置,如有的時候人臉並不正好為方形,有可能$12\times{12}$的影象偏左或偏右,因此需要輸出當前框位置相對完美的人臉框位置的偏移。這個偏移大小為$1\times{1}\times{4}$,即表示框左上角的橫座標的相對偏移,框左上角的縱座標的相對偏移、框的寬度的誤差、框的高度的誤差。
  • 網路的第三部分給出人臉的5個關鍵點的位置。5個關鍵點分別對應著左眼的位置、右眼的位置、鼻子的位置、左嘴巴的位置、右嘴巴的位置。每個關鍵點需要兩維來表示,因此輸出是向量大小為$1\times{1}\times{10}$。

2、R-Net

由於P-Net的檢測時比較粗略的,所以接下來使用R-Net進一步優化。R-Net和P-Net類似,不過這一步的輸入是前面P-Net生成的邊界框,每個邊界框的大小都是$24\times{24}\times{3}$,可以通過縮放得到。網路的輸出和P-Net是一樣的。這一步的目的主要是為了去除大量的非人臉框。

3、O-Net

進一步將R-Net的所得到的區域縮放到$48\times{48}\times{3}$,輸入到最後的O-Net,O-Net的結構與P-Net類似,只不過在測試輸出的時候多了關鍵點位置的輸出。輸入大小為$48\times{48}\times{3}$的影象,輸出包含$P$個邊界框的座標資訊,score以及關鍵點位置。

 從P-Net到R-Net,再到最後的O-Net,網路輸入的影象越來越大,卷積層的通道數越來越多,網路的深度也越來越深,因此識別人臉的準確率應該也是越來越高的。同時P-Net網路的執行速度越快,R-Net次之、O-Net執行速度最慢。之所以使用三個網路,是因為一開始如果直接對影象使用O-Net網路,速度回非常慢。實際上P-Net先做了一層過濾,將過濾後的結果再交給R-Net進行過濾,最後將過濾後的結果交給效果最好但是速度最慢的O-Net進行識別。這樣在每一步都提前減少了需要判別的數量,有效地降低了計算的時間。

二 MTCNN損失函式

 由於MTCNN包含三個子網路,因此其損失函式也由三部分組成。針對人臉識別問題,直接使用交叉熵代價函式,對於框迴歸和關鍵點定位,使用$L2$損失。最後把這三部分的損失各自乘以自身的權重累加起來,形成最後的總損失。在訓練P-Net和R-Net的時候,我們主要關注目標框的準確度,而較少關注關鍵點判定的損失,因此關鍵點損失所佔的權重較小。對於O-Net,比較關注的是關鍵點的位置,因此關鍵點損失所佔的權重就會比較大。

1、人臉識別損失函式

在針對人臉識別的問題,對於輸入樣本$x_i$,我們使用交叉熵代價函式:

$$L_i^{det}=-(y_i^{det}log(p_i) + (1-y_i^{det})(1-log(p_i)))$$

其中$y_i^{det}$表示樣本的真實標籤,$p_i$表示網路輸出為人臉的概率。

2、框迴歸

對於目標框的迴歸,我們採用的是歐氏距離:

$$L_i^{box}=\|\hat{y}_i^{box} - y_i^{box}\|$$

其中$\hat{y}_i^{box}$表示網路輸出之後校正得到的邊界框的座標,$y_i^{box}$是目標的真實邊界框。

3、關鍵點損失函式

對於關鍵點,我們也採用的是歐氏距離:

$$L_i^{landmark}=\|\hat{y}_i^{landmark} - y_i^{landmark}\|$$

其中$\hat{y}_i^{landmark}$表示網路輸出之後得到的關鍵點的座標,$y_i^{landmark}$是關鍵點的真實座標。

 4、總損失

 把上面三個損失函式按照不同的權重聯合起來:

$$min\sum\limits_{i=1}^{N}\sum\limits_{j\in\{det,box,landmark\}}\alpha_j\beta_i^jL_i^j$$

其中$N$是訓練樣本的總數,$\alpha_j$表示各個損失所佔的權重,在P-Net和R-net中,設定$\alpha_{det}=1,\alpha_{box}=0.5,\alpha_{landmark}=0.5$,在O-Net中,設定$\alpha_{det}=1,\alpha_{box}=0.5,\alpha_{landmark}=1$,$\beta_i^j\in\{0,1\}$表示樣本型別指示符。

5、Online Hard sample mining

In particular, in each mini-batch, we sort the losses computed in the forward propagation from all samples and select the top 70% of them as hard samples. Then we only compute the gradients from these hard samples in the backward propagation.That means we ignore the easy samples that are less helpful to strengthen the detector during training. Experiments show that this strategy yields better performance without manual sampleselection.

這段話也就是說,我們在訓練的時候取前向傳播損失值(從大到小)前70%的樣本,來進行反向傳播更新引數。

6、訓練資料

該演算法訓練資料來源於wider和celeba兩個公開的資料庫,wider提供人臉檢測資料,在大圖上標註了人臉框groundtruth的座標資訊,celeba提供了5個landmark點的資料。根據參與任務的不同,將訓練資料分為四類:

  • 負樣本:滑動視窗和Ground True的IOU小於0.3;
  • 正樣本:滑動視窗和Ground True的IOU大於0.65;
  • 中間樣本:滑動視窗和Ground True的IOU大於0.4小於0.65;
  • 關鍵點:包含5個關鍵點做標的;

上面滑動視窗指的是:通過滑動視窗或者隨機取樣的方法獲取尺寸為$12\times{12}$的框:

 三 人臉識別

在上面我們已經介紹了人臉檢測,人臉檢測是人臉相關任務的前提,人臉相關的任務主要有以下幾種:

  • 人臉跟蹤(視訊中跟蹤人臉位置變化);
  • 人臉驗證(輸入兩張人臉,判斷是否屬於同一人);
  • 人臉識別(輸入一張人臉,判斷其屬於人臉資料庫記錄中哪一個人);
  • 人臉聚類(輸入一批人臉,將屬於同一人的自動歸為一類);

下面我們來詳細介紹人臉識別技術:當我們通過MTCNN網路檢測到人臉區域影象時,我們使用深度卷積網路,將輸入的人臉影象轉換為一個向量的表示,也就是所謂的特徵。

那我們如何對人臉提取特徵?我們先來回憶一下VGG16網路,輸入的是影象,經過一系列卷積計算、全連線網路之後,輸出的是類別概率。

 

在通常的影象應用中,可以去掉全連線層,使用卷積層的最後一層當做影象的“特徵”,如上圖中的conv5_3。但如果對人臉識別問題同樣採用這樣的方法,即,使用卷積層最後一層做為人臉的“向量表示”,效果其實是不好的。如何改進?我們之後再談,這裡先談談我們希望這種人臉的“向量表示”應該具有哪些性質。

在理想的狀況下,我們希望“向量表示”之間的距離就可以直接反映人臉的相似度:

  • 對於同一個人的人臉影象,對應的向量的歐幾里得距離應該比較小;

  • 對於不同人的人臉影象,對應的向量之間的歐幾里得距離應該比較大;

例如:設人臉影象為$x_1,x_2$,對應的特徵為$f(x_1),f(x_2)$,當$x_1,x_2$對應是同一個人的人臉時,$f(x_1),f(x_2)$的距離$\|f(x_1)-f(x_2)\|_2$應該很小,而當$x_1,x_2$對應的不是同一個人的人臉時,$f(x_1),f(x_2)$的距離$\|f(x_1)-f(x_2)\|_2$應該很大。

在原始的VGG16模型中,我們使用的是softmax損失,softmax是類別間的損失,對於人臉來說,每一類就是一個人。儘管使用softmax損失可以區別每個人,但其本質上沒有對每一類的向量表示之間的距離做出要求。

舉個例子,使用CNN對MNIST進行分類,我們設計一個特殊的卷積網路,讓最後一層的向量變為2維,此時可以畫出每一類對應的2維向量表示的圖(圖中一種顏色對應一種類別):

上圖是我們直接使用softmax訓練得到的結果,它就不符合我們希望特徵具有的特點:

  • 我們希望同一類對應的向量表示儘可能接近。但這裡同一類(如紫色),可能具有很大的類間距離;

  • 我們希望不同類對應的向量應該儘可能遠。但在圖中靠中心的位置,各個類別的距離都很近;

對於人臉影象同樣會出現類似的情況,對此,有很改進方法。這裡介紹其中兩種:三元組損失函式,中心損失函式。

1、三元組損失

三元組損失函式的原理:既然目標是特徵之間的距離應該具備某些性質,那麼我們就圍繞這個距離來設計損失。具體的,我們每次都在訓練資料中抽出三張人臉影象,第一張影象標記為$x_i^a$,第二章影象標記為$x_i^p$,第三張影象標記為$x_i^n$。在這樣一個"三元組"中,$x_i^a$和$x_i^p$對應的是同一個人的影象,而$x_i^n$是另外一個人的人臉影象。因此距離$\|f(x_i^a)-f(x_i^p)\|_2$應該很小,而距離$\|f(x_i^a)-f(x_i^n)\|_2$應該很大。嚴格來說,三元組損失要求滿足以下不等式:

$$\|f(x_i^a)-f(x_i^p)\|_2^2+\alpha < \|f(x_i^a)-f(x_i^n)\|_2^2$$

即相同人臉間的距離平方至少要比不同人臉間的距離平方小$\alpha$(取平方主要是為了方便求導),據此,設計損失函式為:

$$L_i = [\|f(x_i^a)-f(x_i^p)\|_2^2+\alpha - \|f(x_i^a)-f(x_i^n)\|_2^2]_+$$

這樣的話,當三元組的距離滿足$\|f(x_i^a)-f(x_i^p)\|_2^2+\alpha < \|f(x_i^a)-f(x_i^n)\|_2^2$時,損失$L_i=0$。當距離不滿足上述不等式時,就會有值為$\|f(x_i^a)-f(x_i^p)\|_2^2+\alpha - \|f(x_i^a)-f(x_i^n)\|_2^2$的損失,此外,在訓練時會固定$\|f(x)\|_2=1$,以確保特徵不會無限的"遠離"。

三元組損失直接對距離進行優化,因此可以解決人臉的特徵表示問題。但是在訓練過程中,三元組的選擇非常地有技巧性。如果每次都是隨機選擇三元組,雖然模型可以正確的收斂,但是並不能達到最好的效能。如果加入"難例挖掘",即每次都選擇最難解析度的三元組進行訓練,模型又往往不能正確的收斂。對此,又提出每次都選擇那些"半難"的資料進行訓練,讓模型在可以收斂的同時也保持良好的效能。此外,使用三元組損失訓練人臉模型通常還需要非常大的人臉資料集,才能取得較好的效果。

2、中心損失

與三元組損失不同,中心損失不直接對距離進行優化,它保留了原有的分類模型,但又為每個類(在人臉模型中,一個類就對應一個人)指定了一個類別中心。同一類的影象對應的特徵都應該儘量靠近自己的類別中心,不同類的類別中心儘量遠離。與三元組損失函式,使用中心損失訓練人臉模型不需要使用特別的取樣方法,而且利用較少的影象就可以達到與單元組損失相似的效果。下面我們一起來學習中心損失的定義:

設輸入的人臉影象為$x_i$,該人臉對應的類別是$y_i$,對每個類別都規定一個類別中心,記作$c_{yi}$。希望每個人臉影象對應的特徵$f(x_i)$都儘可能接近中心$c_{yi}$。因此定義損失函式為:

$$L_i=\frac{1}{2}\|f(x_i)-c_{yi}\|_2^2$$

多張影象的中心損失就是將它們的值累加:

$$L_{center}=\sum\limits_{i}L_i$$

這是一個非常簡單的定義。不過還有一個問題沒有解決,那就是如何確定每個類別的中心$c_{yi}$呢?從理論上來說,類別$y_i$的最佳中心應該是它對應所有圖片的特徵的平均值。但如果採用這樣的定義,那麼在每一次梯度下降時,都要對所有圖片計算一次$c_{yi}$,計算複雜度太高了。針對這種情況,不妨近似處理下,在初始階段,先隨機確定$c_{yi}$,接著在每個batch內,使用$L_i=\|f(x_i)-c_{yi}\|_2^2$對當前batch內的$c_{yi}$也計算梯度,並使得該梯度更新$c_{yi}$,此外,不能只使用中心損失來訓練分類模型,還需要加入softmax損失,也就是說,損失最後由兩部分組成,即$L=L_{softmax}+\lambda{L_{center}}$,其中$\lambda$是一個超引數。

最後來總結使用中心損失來訓練人臉模型的過程。首先隨機初始化各個中心$c_{yi}$,接著不斷地取出batch進行訓練,在每個batch中,使用總的損失$L$,除了使用神經網路模型的引數對模型進行更新外,也對$c_{yi}$進行計算梯度,並更新中心的位置。

中心損失可以讓訓練處的特徵具有"內聚性"。還是以MNIST的例子來說,在未加入中心損失時,訓練的結果不具有內聚性。在加入中心損失後,得到的特徵如下:

當中心損失的權重$\lambda$越大時,生成的特徵就會具有越明顯的"內聚性"。

 四 人臉識別的實現

下面我們會介紹一個經典的人臉識別系統-谷歌人臉識別系統facenet,該網路主要包含兩部分:

  • MTCNN部分:用於人臉檢測和人臉對齊,輸出$160\times{160}$大小的影象;
  • CNN部分:可以直接將人臉影象(預設輸入是$160\times{160}$大小)對映到歐幾里得空間,空間距離的長度代表了人臉影象的相似性。只要該對映空間生成、人臉識別,驗證和聚類等任務就可以輕鬆完成;

 

開啟requirements.txt,我們可以看到我們需要安裝以下依賴:

tensorflow==1.7
scipy
scikit-learn
opencv-python
h5py
matplotlib
Pillow
requests
psutil

後面在執行程式時,如果出現安裝包相容問題,建議這裡使用pip安裝,不要使用conda。

1、配置Facenet環境

將facebet資料夾加到python引入庫的預設搜尋路徑中,將facenet檔案整個複製到anaconda3安裝檔案目錄下lib\site-packages下:

然後把剪下src目錄下的檔案,然後刪除facenet下的所有檔案,貼上src目錄下的檔案到facenet下,這樣做的目的是為了匯入src目錄下的包(這樣import align.detect_face不會報錯)。

在Anaconda Prompt中執行python,輸入import facenet,不報錯即可:

2、下載LFW資料集

接下來將會講解如何使用已經訓練好的模型在LFW(Labeled Faces in the Wild)資料庫上測試,不過我還需要先來介紹一下LFW資料集。

LFW資料集是由美國馬賽諸塞大學阿姆斯特分校計算機實驗室整理的人臉檢測資料集,是評估人臉識別演算法效果的公開測試資料集。LFW資料集共有13233張jpeg格式圖片,屬於5749個不同的人,其中有1680人對應不止一張圖片,每張圖片尺寸都是$250\times{250}$,並且被標示出對應的人的名字。LFW資料集中每張圖片命名方式為"lfw/name/name_xxx.jpg",這裡"xxx"是前面補零的四點陣圖片編號。例如,前美國總統喬治布什的第十張圖片為"lfw/George_W_Bush/George_W_Bush_0010.jpg"。

在lfw下新建一個資料夾raw,把lfw中所有的檔案(除了raw)移到raw資料夾中。可以看到我的資料集lfw是放在datasets資料夾下,其中datasets資料夾和facenet是在同一路徑下。

3、LFW資料集預處理(LFW資料庫上的人臉檢測和對齊)

我們需要將檢測所使用的資料集校準為和訓練模型所使用的資料集大小一致($160\times{160}$),轉換後的資料集儲存在lfw_mtcnnpy_160資料夾內,

處理的第一步是使用MTCNN網路進行人臉檢測和對齊,並縮放到$160\times{160}$。

MTCNN的實現主要在資料夾facenet/src/align中,資料夾的內容如下:

  • detect_face.py:定義了MTCNN的模型結構,由P-Net、R-Net、O-Net組成,這三個網路已經提供了預訓練的模型,模型資料分別對應檔案det1.npy、det2.npy、det3.npy。
  • align_dataset_matcnn.py:是使用MTCNN的模型進行人臉檢測和對齊的入口程式碼。

使用指令碼align_dataset_mtcnn.py對LFW資料庫進行人臉檢測和對齊的方法通過執行命令,我們開啟Anaconda Prompt,來到facenet所在的路徑下,執行如下命令:

python  facenet/src/align/align_dataset_mtcnn.py   datasets/lfw/raw  datasets/lfw/lfw_mtcnnpy_160 --image_size 160 --margin 32 --random_order

該命令會建立一個datasets/lfw/lfw_mtcnnpy_160的資料夾,並將所有對齊好的人臉影象存放到這個資料夾中,資料的結構和原先的datasets/lfw/raw一樣。引數--image_size 160 --margin 32的含義是在MTCNN檢測得到的人臉框的基礎上縮小32畫素(訓練時使用的資料偏大),並縮放到$160\times{160}$大小,因此最後得到的對齊後的影象都是$160\times{160}$畫素的,這樣的話,就成功地從原始影象中檢測並對齊了人臉。

下面我們來簡略的分析一下align_dataset_mtcnn.py原始檔,先上原始碼如下,然後我們來解讀一下main()函式

"""Performs face alignment and stores face thumbnails in the output directory."""
# MIT License
# 
# Copyright (c) 2016 David Sandberg
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from scipy import misc
import sys
import os
import argparse
import tensorflow as tf
import numpy as np
import facenet
import align.detect_face
import random
from time import sleep



'''
使用MTCNN網路進行人臉檢測和對齊
'''

def main(args):
    '''
    args:
        args:引數,關鍵字引數
    '''
    
    sleep(random.random())
    #設定對齊後的人臉影象存放的路徑
    output_dir = os.path.expanduser(args.output_dir)
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    # Store some git revision info in a text file in the log directory  儲存一些配置引數等資訊
    src_path,_ = os.path.split(os.path.realpath(__file__))
    facenet.store_revision_info(src_path, output_dir, ' '.join(sys.argv))
    
    '''1、獲取LFW資料集 獲取每個類別名稱以及該類別下所有圖片的絕對路徑'''
    dataset = facenet.get_dataset(args.input_dir)
    
    print('Creating networks and loading parameters')
    
    '''2、建立MTCNN網路,並預訓練(即使用訓練好的網路初始化引數)'''
    with tf.Graph().as_default():
        gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=args.gpu_memory_fraction)
        sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options, log_device_placement=False))
        with sess.as_default():
            pnet, rnet, onet = align.detect_face.create_mtcnn(sess, None)
    
    minsize = 20                   # minimum size of face
    threshold = [ 0.6, 0.7, 0.7 ]  # three steps's threshold
    factor = 0.709                 # scale factor

    # Add a random key to the filename to allow alignment using multiple processes
    random_key = np.random.randint(0, high=99999)
    bounding_boxes_filename = os.path.join(output_dir, 'bounding_boxes_%05d.txt' % random_key)
    
    '''3、每個圖片中人臉所在的邊界框寫入記錄檔案中'''
    with open(bounding_boxes_filename, "w") as text_file:
        nrof_images_total = 0
        nrof_successfully_aligned = 0
        if args.random_order:
            random.shuffle(dataset)
        #獲取每一個人,以及對應的所有圖片的絕對路徑
        for cls in dataset:
            #每一個人對應的輸出資料夾
            output_class_dir = os.path.join(output_dir, cls.name)
            if not os.path.exists(output_class_dir):
                os.makedirs(output_class_dir)
                if args.random_order:
                    random.shuffle(cls.image_paths)
            #遍歷每一張圖片
            for image_path in cls.image_paths:
                nrof_images_total += 1
                filename = os.path.splitext(os.path.split(image_path)[1])[0]
                output_filename = os.path.join(output_class_dir, filename+'.png')
                print(image_path)
                if not os.path.exists(output_filename):
                    try:
                        img = misc.imread(image_path)
                    except (IOError, ValueError, IndexError) as e:
                        errorMessage = '{}: {}'.format(image_path, e)
                        print(errorMessage)
                    else:
                        if img.ndim<2:
                            print('Unable to align "%s"' % image_path)
                            text_file.write('%s\n' % (output_filename))
                            continue
                        if img.ndim == 2:
                            img = facenet.to_rgb(img)
                        img = img[:,:,0:3]
    
                        #人臉檢測 bounding_boxes:表示邊界框 形狀為[n,5] 5對應x1,y1,x2,y2,score
                        #_:人臉關鍵點座標 形狀為 [n,10]
                        bounding_boxes, _ = align.detect_face.detect_face(img, minsize, pnet, rnet, onet, threshold, factor)
                        #邊界框個數
                        nrof_faces = bounding_boxes.shape[0]
                        if nrof_faces>0:
                            #[n,4] 人臉框
                            det = bounding_boxes[:,0:4]
                            #儲存所有人臉框
                            det_arr = []
                            img_size = np.asarray(img.shape)[0:2]
                            if nrof_faces>1:
                                #一張圖片中檢測多個人臉
                                if args.detect_multiple_faces:
                                    for i in range(nrof_faces):
                                        det_arr.append(np.squeeze(det[i]))
                                else:
                                    bounding_box_size = (det[:,2]-det[:,0])*(det[:,3]-det[:,1])
                                    img_center = img_size / 2
                                    offsets = np.vstack([ (det[:,0]+det[:,2])/2-img_center[1], (det[:,1]+det[:,3])/2-img_center[0] ])
                                    offset_dist_squared = np.sum(np.power(offsets,2.0),0)
                                    index = np.argmax(bounding_box_size-offset_dist_squared*2.0) # some extra weight on the centering
                                    det_arr.append(det[index,:])
                            else:
                                #只有一個人臉框
                                det_arr.append(np.squeeze(det))

                            #遍歷每一個人臉框
                            for i, det in enumerate(det_arr):
                                #[4,]  邊界框擴大margin區域,並進行裁切
                                det = np.squeeze(det)
                                bb = np.zeros(4, dtype=np.int32)
                                bb[0] = np.maximum(det[0]-args.margin/2, 0)
                                bb[1] = np.maximum(det[1]-args.margin/2, 0)
                                bb[2] = np.minimum(det[2]+args.margin/2, img_size[1])
                                bb[3] = np.minimum(det[3]+args.margin/2, img_size[0])
                                cropped = img[bb[1]:bb[3],bb[0]:bb[2],:]
                                #縮放到指定大小,並儲存圖片,以及邊界框位置資訊
                                scaled = misc.imresize(cropped, (args.image_size, args.image_size), interp='bilinear')
                                nrof_successfully_aligned += 1
                                filename_base, file_extension = os.path.splitext(output_filename)
                                if args.detect_multiple_faces:
                                    output_filename_n = "{}_{}{}".format(filename_base, i, file_extension)
                                else:
                                    output_filename_n = "{}{}".format(filename_base, file_extension)
                                misc.imsave(output_filename_n, scaled)
                                text_file.write('%s %d %d %d %d\n' % (output_filename_n, bb[0], bb[1], bb[2], bb[3]))
                        else:
                            print('Unable to align "%s"' % image_path)
                            text_file.write('%s\n' % (output_filename))
                            
    print('Total number of images: %d' % nrof_images_total)
    print('Number of successfully aligned images: %d' % nrof_successfully_aligned)
            

def parse_arguments(argv):
    '''
    解析命令列引數
    '''
    parser = argparse.ArgumentParser()
        
    #定義引數  input_dir、output_dir為外部引數名
    parser.add_argument('input_dir', type=str, help='Directory with unaligned images.')
    parser.add_argument('output_dir', type=str, help='Directory with aligned face thumbnails.')
    parser.add_argument('--image_size', type=int,
        help='Image size (height, width) in pixels.', default=160)
    parser.add_argument('--margin', type=int,
        help='Margin for the crop around the bounding box (height, width) in pixels.', default=32)
    parser.add_argument('--random_order', 
        help='Shuffles the order of images to enable alignment using multiple processes.', action='store_true')
    parser.add_argument('--gpu_memory_fraction', type=float,
        help='Upper bound on the amount of GPU memory that will be used by the process.', default=1.0)
    parser.add_argument('--detect_multiple_faces', type=bool,
                        help='Detect and align multiple faces per image.', default=False)
    #解析
    return parser.parse_args(argv)

if __name__ == '__main__':
    main(parse_arguments(sys.argv[1:]))
View Code
  • 首先載入LFW資料集;
  • 建立MTCNN網路,並預訓練(即使用訓練好的網路初始化引數),Google Facenet的作者在建立網路時,自己重寫了CNN網路所需的各個元件,包括conv層,MaxPool層,Softmax層等等,由於作者寫的比較複雜。有興趣的同學看看MTCNN 的 TensorFlow 實現這篇部落格,博主使用Keras重新實現了MTCNN網路,也比較好懂程式碼連結:https://github.com/FortiLeiZhang/model_zoo/tree/master/TensorFlow/mtcnn
  • 呼叫align.detect_face.detect_face()函式進行人臉檢測,返回校準後的人臉邊界框的位置、score、以及關鍵點座標;
  • 對人臉框進行處理,從原圖中裁切(先進行了邊緣擴充套件32個畫素)、以及縮放(縮放到$160\times{160}$)等,並儲存相關資訊到檔案;

關於人臉檢測的具體細節可以檢視detect_face()函式,程式碼也比較長,這裡我放上程式碼,具體細節部分可以參考MTCNN 的 TensorFlow 實現這篇部落格。

def detect_face(img, minsize, pnet, rnet, onet, threshold, factor):
    """Detects faces in an image, and returns bounding boxes and points for them.
    img: input image
    minsize: minimum faces' size
    pnet, rnet, onet: caffemodel
    threshold: threshold=[th1, th2, th3], th1-3 are three steps's threshold
    factor: the factor used to create a scaling pyramid of face sizes to detect in the image.
    """
    factor_count=0
    total_boxes=np.empty((0,9))
    points=np.empty(0)
    h=img.shape[0]
    w=img.shape[1]
    #最小值 假設是250x250
    minl=np.amin([h, w])
    #假設最小人臉 minsize=20,由於我們P-Net人臉檢測視窗大小為12x12,
    #因此必須縮放才能使得檢測視窗檢測到完整的人臉 m=0.6
    m=12.0/minsize
    #180
    minl=minl*m
    # create scale pyramid   不同尺度金字塔,儲存每個尺度縮放尺度係數0.6  0.6*0.7 ...
    scales=[]
    while minl>=12:
        scales += [m*np.power(factor, factor_count)]
        minl = minl*factor
        factor_count += 1

    # first stage  P-Net
    for scale in scales:
        #縮放影象
        hs=int(np.ceil(h*scale))
        ws=int(np.ceil(w*scale))
        im_data = imresample(img, (hs, ws))
        #歸一化[-1,1]之間
        im_data = (im_data-127.5)*0.0078125
        img_x = np.expand_dims(im_data, 0)
        img_y = np.transpose(img_x, (0,2,1,3))
        out = pnet(img_y)
        #輸入影象是[1,150,150,3] 則輸出為[1,70,70,4] 邊界框,每一個特徵點都對應一個12x12大小檢測視窗
        out0 = np.transpose(out[0], (0,2,1,3))
        #輸入影象是[1,150,150,3] 則輸出為[1,70,70,2] 人臉概率
        out1 = np.transpose(out[1], (0,2,1,3))
        
        #輸出為[n,9] 前4位為人臉框在原圖中的位置,第5位為判定為人臉的概率,後4位為框迴歸的值
        boxes, _ = generateBoundingBox(out1[0,:,:,1].copy(), out0[0,:,:,:].copy(), scale, threshold[0])
        
        # inter-scale nms  非極大值抑制,然後儲存剩下的bb
        pick = nms(boxes.copy(), 0.5, 'Union')
        if boxes.size>0 and pick.size>0:
            boxes = boxes[pick,:]
            total_boxes = np.append(total_boxes, boxes, axis=0)
    
    #圖片按照所有scale走完一遍,會得到在原圖上基於不同scale的所有的bb,然後對這些bb再進行一次NMS
    #並且這次NMS的threshold要提高
    numbox = total_boxes.shape[0]
    if numbox>0:
        pick = nms(total_boxes.copy(), 0.7, 'Union')
        total_boxes = total_boxes[pick,:]
        #使用框迴歸校準bb  框迴歸:框左上角的橫座標的相對偏移,框左上角的縱座標的相對偏移、框的寬度的誤差、框的高度的誤差。
        regw = total_boxes[:,2]-total_boxes[:,0]
        regh = total_boxes[:,3]-total_boxes[:,1]
        qq1 = total_boxes[:,0]+total_boxes[:,5]*regw
        qq2 = total_boxes[:,1]+total_boxes[:,6]*regh
        qq3 = total_boxes[:,2]+total_boxes[:,7]*regw
        qq4 = total_boxes[:,3]+total_boxes[:,8]*regh
        #[n,8]
        total_boxes = np.transpose(np.vstack([qq1, qq2, qq3, qq4, total_boxes[:,4]]))
        #把每一個bb轉換為正方形
        total_boxes = rerec(total_boxes.copy())
        total_boxes[:,0:4] = np.fix(total_boxes[:,0:4]).astype(np.int32)
        #把超過原圖邊界