僅使用OpenCV實現(xiàn)活體檢測!(附源碼)
點擊上方“小白學視覺”,選擇加"星標"或“置頂”
重磅干貨,第一時間送達
我們先來看一個網(wǎng)絡留言

△?來自蝦米媽咪
小朋友用媽媽的一寸照片通過了人臉識別,打擊了小度音箱的家長監(jiān)督機制。
活體檢測沒做好。
公交車身廣告上的董明珠頭像,被寧波交警系統(tǒng)拍了照,判定成“違法闖紅燈”。
活體檢測沒做好。
所以,活體檢測要怎么做?
名叫Adrian Rosebrock的程序猿,寫了份事無巨細的教程,從構建數(shù)據(jù)集開始,一步步教大家用AI分辨真人和照片,精細到每行代碼的用途。

△?川川是假的,光頭是真的
這個識別方法,用到了OpenCV和Keras,打開攝像頭就可以實時檢測。
重要的是,有源碼提供,受到了推特用戶的踴躍比心。
AI可以用哪些技巧,來區(qū)分真人和照片?
一是紋理分析?(Texture Analysis) 。皮膚的紋理特征是重要的依據(jù),給2D照片拍照,比起給3D真人拍照,會損失一些紋理。
二是頻率分析?(Frequency Analysis) 。照片臉部的頻率組成,也不像真人那樣豐富。
三是可變聚焦分析?(Variable focusing Analysis) 。連拍兩張照片,聚焦在不同位置,查看像素值 (Pixel Value) 的變化。
四是啟發(fā)式算法?(Heuristic-Based Algorithms) 。眼動、唇動、眨眼這些動作,照片是不會有的。
五是光流算法?(Optical Flow Algorithms) 。在相鄰兩幀之間,檢測物體運動的方向和幅度,查出2D和3D物體之間的差別。
……

不過這里,就把活體檢測看成一個粗暴的二分類問題,這些復雜的分析先拋到一邊。
程序猿把問題又簡化了一下:這里說的“假臉”,只是“屏幕里的人臉”而已。
現(xiàn)在,可以開始造數(shù)據(jù)集了。
他用手機的前攝像頭拍了一段25秒的視頻;又舉著手機、對著電腦攝像頭,把視頻播了一遍。

這樣,一段真人視頻,和一段“假臉”視頻,就準備好了。
程序猿拍下的這兩條視頻,都提供下載。不過,他還是建議大家多收集一些數(shù)據(jù),不同的人臉,甚至不同的人種,幫助算法茁壯成長。
下一步,要用OpenCV的人臉檢測算法處理兩段視頻,把有用的區(qū)域 (ROI) ,就是人臉部分框出來。
這步用了80多行代碼,每一行在做些什么,教程里都寫清了。

最后,按照真假兩個類別,把框好的臉部提取出來,就有了數(shù)據(jù)集。
溫馨提示,需要平衡一下真圖和假圖的數(shù)量。比如,程序猿發(fā)現(xiàn)真實視頻比假視頻長,就用更大的間隔來提取。于是,他收獲了真圖161張,假圖150張。
這數(shù)據(jù)集還是顯得有些貧乏,所以后面需要擴增:旋轉(zhuǎn)、翻轉(zhuǎn)之類的操作,可以增加圖片數(shù)量。
數(shù)據(jù)集做好了,就要喂給做活體檢測的神經(jīng)網(wǎng)絡。
程序猿給網(wǎng)絡起名LivenessNet,大體長這樣:

他說,這其實只是一個簡單的CNN。而且,已經(jīng)努力讓網(wǎng)絡保持在最淺、參數(shù)最少的狀態(tài)。
這樣做有兩個原因:一是為避免模型在小數(shù)據(jù)集上發(fā)生過擬合,二是為保證模型快到可以實時推理,就算在樹莓派上也能運行。
搭個網(wǎng)絡
現(xiàn)在,就來實現(xiàn)一下這個網(wǎng)絡。打開livenessnet.py,再寫這一段代碼:
1#?import?the?necessary?packages
2from?keras.models?import?Sequential
3from?keras.layers.normalization?import?BatchNormalization
4from?keras.layers.convolutional?import?Conv2D
5from?keras.layers.convolutional?import?MaxPooling2D
6from?keras.layers.core?import?Activation
7from?keras.layers.core?import?Flatten
8from?keras.layers.core?import?Dropout
9from?keras.layers.core?import?Dense
10from?keras?import?backend?as?K
11
12class?LivenessNet:
13????@staticmethod
14????def?build(width,?height,?depth,?classes):
15????????#?initialize?the?model?along?with?the?input?shape?to?be
16????????#?"channels?last"?and?the?channels?dimension?itself
17????????model?=?Sequential()
18????????inputShape?=?(height,?width,?depth)
19????????chanDim?=?-1
20
21????????#?if?we?are?using?"channels?first",?update?the?input?shape
22????????#?and?channels?dimension
23????????if?K.image_data_format()?==?"channels_first":
24????????????inputShape?=?(depth,?height,?width)
25????????????chanDim?=?1
然后,一層一層加上去:
1????????#?first?CONV?=>?RELU?=>?CONV?=>?RELU?=>?POOL?layer?set
2????????model.add(Conv2D(16,?(3,?3),?padding="same",
3????????????input_shape=inputShape))
4????????model.add(Activation("relu"))
5????????model.add(BatchNormalization(axis=chanDim))
6????????model.add(Conv2D(16,?(3,?3),?padding="same"))
7????????model.add(Activation("relu"))
8????????model.add(BatchNormalization(axis=chanDim))
9????????model.add(MaxPooling2D(pool_size=(2,?2)))
10????????model.add(Dropout(0.25))
11
12????????#?second?CONV?=>?RELU?=>?CONV?=>?RELU?=>?POOL?layer?set
13????????model.add(Conv2D(32,?(3,?3),?padding="same"))
14????????model.add(Activation("relu"))
15????????model.add(BatchNormalization(axis=chanDim))
16????????model.add(Conv2D(32,?(3,?3),?padding="same"))
17????????model.add(Activation("relu"))
18????????model.add(BatchNormalization(axis=chanDim))
19????????model.add(MaxPooling2D(pool_size=(2,?2)))
20????????model.add(Dropout(0.25))程序猿說,這個網(wǎng)絡有點像VGGNet,很淺,過濾器 (Filter) 很少。只是判斷真假,不用深度網(wǎng)絡。
最后,再加一個FC層→RELU層的組合。
1????????#?first?(and?only)?set?of?FC?=>?RELU?layers
2????????model.add(Flatten())
3????????model.add(Dense(64))
4????????model.add(Activation("relu"))
5????????model.add(BatchNormalization())
6????????model.add(Dropout(0.5))
7
8????????#?softmax?classifier
9????????model.add(Dense(classes))
10????????model.add(Activation("softmax"))
11
12????????#?return?the?constructed?network?architecture
13????????return?modelCNN搭好了,該訓練了。
訓練腳本長這樣
大致的訓練過程,就像這張圖:

打開train_liveness.py,寫下這段代碼:
1#?set?the?matplotlib?backend?so?figures?can?be?saved?in?the?background
2import?matplotlib
3matplotlib.use("Agg")
4
5#?import?the?necessary?packages
6from?pyimagesearch.livenessnet?import?LivenessNet
7from?sklearn.preprocessing?import?LabelEncoder
8from?sklearn.model_selection?import?train_test_split
9from?sklearn.metrics?import?classification_report
10from?keras.preprocessing.image?import?ImageDataGenerator
11from?keras.optimizers?import?Adam
12from?keras.utils?import?np_utils
13from?imutils?import?paths
14import?matplotlib.pyplot?as?plt
15import?numpy?as?np
16import?argparse
17import?pickle
18import?cv2
19import?os
20
21#?construct?the?argument?parser?and?parse?the?arguments
22ap?=?argparse.ArgumentParser()
23ap.add_argument("-d",?"--dataset",?required=True,
24????help="path?to?input?dataset")
25ap.add_argument("-m",?"--model",?type=str,?required=True,
26????help="path?to?trained?model")
27ap.add_argument("-l",?"--le",?type=str,?required=True,
28????help="path?to?label?encoder")
29ap.add_argument("-p",?"--plot",?type=str,?default="plot.png",
30????help="path?to?output?loss/accuracy?plot")
31args?=?vars(ap.parse_args())

△?導入,不停地導入
里面包含了許多的導入:
有matplotlib,是可視化工具;
有LivenessNet,就是剛才搭好的CNN;
有train_test_split,這是scikit-learn里的函數(shù),把數(shù)據(jù)集拆成訓練集和測試集;
有classification_report,也是scikit-learn里面的工具,用來生成簡短統(tǒng)計報告的;
有ImageDataGenerator,做數(shù)據(jù)擴增用的;
有Adam,適合這個任務的優(yōu)化器,當然也可以用SGD、RMSprop等等代替;
有paths,這個模塊是用來收集圖片路徑的;
有pyplot,也是美麗的可視化工具;
有numpy,是Python數(shù)學庫,也是OpenCV必需品;
有argparse,用來處理命令行參數(shù);
有pickle,可以把標簽編碼器序列化到盤上;
有cv2,這是一組OpenCV Binding;
還有os,這個模塊用處很多,但這里只用到了它的操作系統(tǒng)路徑分隔符而已。

梳理好這些,再看余下的代碼,就會清晰很多了。
后面,是一系列的初始化,以及訓練前的各種準備活動。此處略去,詳見教程原文。
訓練正式啟動
準備就緒,運行這段命令,就可以訓練了:
1$?python?train.py?--dataset?dataset?--model?liveness.model?--le?le.pickle
2[INFO]?loading?images...
3[INFO]?compiling?model...
4[INFO]?training?network?for?50?epochs...
5Epoch?1/50
629/29?[==============================]?-?2s?58ms/step?-?loss:?1.0113?-?acc:?0.5862?-?val_loss:?0.4749?-?val_acc:?0.7436
7Epoch?2/50
829/29?[==============================]?-?1s?21ms/step?-?loss:?0.9418?-?acc:?0.6127?-?val_loss:?0.4436?-?val_acc:?0.7949
9Epoch?3/50
1029/29?[==============================]?-?1s?21ms/step?-?loss:?0.8926?-?acc:?0.6472?-?val_loss:?0.3837?-?val_acc:?0.8077
11...
12Epoch?48/50
1329/29?[==============================]?-?1s?21ms/step?-?loss:?0.2796?-?acc:?0.9094?-?val_loss:?0.0299?-?val_acc:?1.0000
14Epoch?49/50
1529/29?[==============================]?-?1s?21ms/step?-?loss:?0.3733?-?acc:?0.8792?-?val_loss:?0.0346?-?val_acc:?0.9872
16Epoch?50/50
1729/29?[==============================]?-?1s?21ms/step?-?loss:?0.2660?-?acc:?0.9008?-?val_loss:?0.0322?-?val_acc:?0.9872
18[INFO]?evaluating?network...
19??????????????precision????recall??f1-score???support
20
21????????fake???????0.97??????1.00??????0.99????????35
22????????real???????1.00??????0.98??????0.99????????43
23
24???micro?avg???????0.99??????0.99??????0.99????????78
25???macro?avg???????0.99??????0.99??????0.99????????78
26weighted?avg???????0.99??????0.99??????0.99????????78
27
28[INFO]?serializing?network?to?'liveness.model'...成果斐然
訓練完結,LivenessNet在驗證集上拿到了99%的準確度。
當然,驗證集只是熱身,后面還要打開攝像頭,讓AI去看更多沒見過的人,和沒見過的“假人”。
(這一部分代碼,也有詳盡的解讀,參見教程原文。)

就像開頭展示的那樣,AI能清楚地判斷,它眼前的川川不是真人,而程序猿是真人。
那么,你也可以訓練一只這樣的AI了。
不過,不用局限于簡單的二分類,可以用上前面講到的那些復雜的分析方法,比如頻率分析,比如光流法,大有可為。
教程原文傳送門:
https://www.pyimagesearch.com/2019/03/11/liveness-detection-with-opencv/
源碼下載入口,和無微不至的代碼解析,都在里面了。

交流群
歡迎加入公眾號讀者群一起和同行交流,目前有SLAM、三維視覺、傳感器、自動駕駛、計算攝影、檢測、分割、識別、醫(yī)學影像、GAN、算法競賽等微信群(以后會逐漸細分),請掃描下面微信號加群,備注:”昵稱+學校/公司+研究方向“,例如:”張三?+?上海交大?+?視覺SLAM“。請按照格式備注,否則不予通過。添加成功后會根據(jù)研究方向邀請進入相關微信群。請勿在群內(nèi)發(fā)送廣告,否則會請出群,謝謝理解~
