作者|Jordan Van Eetveldt
編譯|Flin
來源|towardsdatascience
你在互聯網上找到的大多數人臉識別算法和研究論文都遭受照片攻擊。這些方法在檢測和識別來自網絡攝像頭的圖像、視頻和視頻流中的人臉方面非常有效。然而,他們無法區分現實生活中的面孔和照片上的面孔。這種無法識別人臉的現象是由於這些算法在二維幀上工作。
現在讓我們想象一下我們想要實現一個人臉識別開門器。該系統可以很好地區分已知面孔和未知面孔,以便只有授權人員才能訪問。盡管如此,一個心懷不軌的人只要出示授權人的照片。這個3D探測器,類似於蘋果的FaceID,應運而生了。但如果我們沒有3D探測器呢?
本文的目標是實現一種基於眨眼檢測的人臉活體檢測算法,以抵抗照片攻擊。該算法通過網絡攝像頭實時工作,只有當人的名字閃爍時才會顯示出來。通俗地說,程序運行如下:
- 在網絡攝像頭生成的每個幀中檢測人臉。
- 對於每個檢測到的臉,檢測眼睛。
- 對於每個檢測到的眼睛,檢測眼睛是否睜開或關閉。
- 如果在某個時候檢測到眼睛是睜開的,然后是閉着的,然后是睜開的,我們就斷定此人已經眨了眼睛,並且程序顯示了他的名字(如果是人臉識別開門器,我們將授權此人進入)。
對於人臉的檢測和識別,你需要安裝face_recognition庫,它提供了非常有用的深度學習方法來查找和識別圖像中的人臉。特別是,face_locations、face_encodings和compare_faces函數是最有用的3個函數。人臉定位方法可以用兩種方法來檢測人臉:方向梯度直方圖(HoG)和卷積神經網絡(CNN)。由於時間限制,選擇了HoG方法。
face_encodings函數是一個預先訓練的卷積神經網絡,能夠將圖像編碼成128個特征向量。這個嵌入向量應該表示足夠的信息來區分兩個不同的人。最后,compare_faces計算兩個嵌入向量之間的距離。它將允許算法識別從攝像頭幀中提取的人臉,並將其嵌入向量與我們數據集中所有編碼的人臉進行比較。最近的向量應該對應於同一個人。
1. 已知人臉數據集編碼
在我的例子中,算法能夠識別我和奧巴馬。我為每個人挑選了大約10張照片。下面是處理和編碼已知人臉數據庫的代碼。
def process_and_encode(images):
known_encodings = []
known_names = []
print("[LOG] Encoding dataset ...")
for image_path in tqdm(images):
# 加載圖片
image = cv2.imread(image_path)
# 將其從BGR轉換為RGB
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 檢測圖像中的臉並獲取其位置(方框坐標)
boxes = face_recognition.face_locations(image, model='hog')
# 將人臉編碼為128維嵌入向量
encoding = face_recognition.face_encodings(image, boxes)
# 人物名稱是圖像來源文件夾的名稱
name = image_path.split(os.path.sep)[-2]
if len(encoding) > 0 :
known_encodings.append(encoding[0])
known_names.append(name)
return {"encodings": known_encodings, "names": known_names}
現在我們知道了每個想識別的人的編碼,我們可以嘗試通過網絡攝像頭識別人臉。然而,在轉到這一部分之前,我們需要區分一張人臉照片和一張活人的臉。
- 人臉活體檢測
作為提醒,我們的目標是在某個點上檢測出一個睜閉的睜眼模式。我訓練了一個卷積神經網絡來分類眼睛是閉着的還是睜着的。所選擇的模型是LeNet-5,它已經在Closed Eyes In The Wild (CEW)數據集上進行了訓練。它由大約4800張24x24大小的眼睛圖像組成。
Closed Eyes In The Wild (CEW)數據集地址:
from keras.models import Sequential
from keras.layers import Conv2D
from keras.layers import AveragePooling2D
from keras.layers import Flatten
from keras.layers import Dense
from keras.preprocessing.image import ImageDataGenerator
IMG_SIZE = 24
def train(train_generator, val_generator):
STEP_SIZE_TRAIN=train_generator.n//train_generator.batch_size
STEP_SIZE_VALID=val_generator.n//val_generator.batch_size
model = Sequential()
model.add(Conv2D(filters=6, kernel_size=(3, 3), activation='relu', input_shape=(IMG_SIZE,IMG_SIZE,1)))
model.add(AveragePooling2D())
model.add(Conv2D(filters=16, kernel_size=(3, 3), activation='relu'))
model.add(AveragePooling2D())
model.add(Flatten())
model.add(Dense(units=120, activation='relu'))
model.add(Dense(units=84, activation='relu'))
model.add(Dense(units=1, activation = 'sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
print('[LOG] Training CNN')
model.fit_generator(generator=train_generator,
steps_per_epoch=STEP_SIZE_TRAIN,
validation_data=val_generator,
validation_steps=STEP_SIZE_VALID,
epochs=20
)
return model
在評估模型時,我達到了94%的准確率。
每次我們檢測到一只眼睛,我們就用我們的模型來預測它的狀態,並跟蹤每個人的眼睛狀態。因此,檢測眨眼變得非常容易,它試圖在眼睛狀態歷史中找到一個閉眼-睜眼-閉眼模式。
def isBlinking(history, maxFrames):
""" @history: A string containing the history of eyes status
where a '1' means that the eyes were closed and '0' open.
@maxFrames: The maximal number of successive frames where an eye is closed """
for i in range(maxFrames):
pattern = '1' + '0'*(i+1) + '1'
if pattern in history:
return True
return False
- 活體的人臉識別
我們幾乎擁有建立“真實”人臉識別算法的所有要素。我們只需要一種實時檢測人臉和眼睛的方法。我使用openCV預先訓練的Haar級聯分類器來完成這些任務。有關Haar cascade人臉和眼睛檢測的更多信息,我強烈建議你閱讀openCV的這篇強大的文章。
def detect_and_display(model, video_capture, face_detector, open_eyes_detector, left_eye_detector, right_eye_detector, data, eyes_detected):
frame = video_capture.read()
# 調整框架大小
frame = cv2.resize(frame, (0, 0), fx=0.6, fy=0.6)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 檢測人臉
faces = face_detector.detectMultiScale(
gray,
scaleFactor=1.2,
minNeighbors=5,
minSize=(50, 50),
flags=cv2.CASCADE_SCALE_IMAGE
)
# 對於每個檢測到的臉
for (x,y,w,h) in faces:
# 將人臉編碼為128維嵌入向量
encoding = face_recognition.face_encodings(rgb, [(y, x+w, y+h, x)])[0]
# 將向量與所有已知的人臉編碼進行比較
matches = face_recognition.compare_faces(data["encodings"], encoding)
# 目前我們不知道該人的名字
name = "Unknown"
# 如果至少有一次匹配:
if True in matches:
matchedIdxs = [i for (i, b) in enumerate(matches) if b]
counts = {}
for i in matchedIdxs:
name = data["names"][i]
counts[name] = counts.get(name, 0) + 1
# 匹配次數最多的已知編碼對應於檢測到的人臉名稱
name = max(counts, key=counts.get)
face = frame[y:y+h,x:x+w]
gray_face = gray[y:y+h,x:x+w]
eyes = []
# 眼睛檢測
# 首先檢查眼睛是否睜開(考慮到眼鏡)
open_eyes_glasses = open_eyes_detector.detectMultiScale(
gray_face,
scaleFactor=1.1,
minNeighbors=5,
minSize=(30, 30),
flags = cv2.CASCADE_SCALE_IMAGE
)
# 如果open_eyes_glasses檢測到眼睛,則眼睛睜開
if len(open_eyes_glasses) == 2:
eyes_detected[name]+='1'
for (ex,ey,ew,eh) in open_eyes_glasses:
cv2.rectangle(face,(ex,ey),(ex+ew,ey+eh),(0,255,0),2)
# 否則嘗試使用left和right_eye_detector檢測眼睛
# 以檢測到睜開和閉合的眼睛
else:
# 將臉分成左右兩邊
left_face = frame[y:y+h, x+int(w/2):x+w]
left_face_gray = gray[y:y+h, x+int(w/2):x+w]
right_face = frame[y:y+h, x:x+int(w/2)]
right_face_gray = gray[y:y+h, x:x+int(w/2)]
# 檢測左眼
left_eye = left_eye_detector.detectMultiScale(
left_face_gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(30, 30),
flags = cv2.CASCADE_SCALE_IMAGE
)
# 檢測右眼
right_eye = right_eye_detector.detectMultiScale(
right_face_gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(30, 30),
flags = cv2.CASCADE_SCALE_IMAGE
)
eye_status = '1' # we suppose the eyes are open
# 檢查每只眼睛是否閉合。
# 如果有人閉着眼睛,我們得出結論是閉着眼睛
for (ex,ey,ew,eh) in right_eye:
color = (0,255,0)
pred = predict(right_face[ey:ey+eh,ex:ex+ew],model)
if pred == 'closed':
eye_status='0'
color = (0,0,255)
cv2.rectangle(right_face,(ex,ey),(ex+ew,ey+eh),color,2)
for (ex,ey,ew,eh) in left_eye:
color = (0,255,0)
pred = predict(left_face[ey:ey+eh,ex:ex+ew],model)
if pred == 'closed':
eye_status='0'
color = (0,0,255)
cv2.rectangle(left_face,(ex,ey),(ex+ew,ey+eh),color,2)
eyes_detected[name] += eye_status
# 每次,我們都會檢查該人是否眨眼
# 如果是,我們顯示其名字
if isBlinking(eyes_detected[name],3):
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
# 顯示名字
y = y - 15 if y - 15 > 15 else y + 15
cv2.putText(frame, name, (x, y), cv2.FONT_HERSHEY_SIMPLEX,0.75, (0, 255, 0), 2)
return frame
上面的功能是用於檢測和識別真實人臉的代碼。它接受以下參數:
-
model:我們的睜眼/閉眼分類器
-
video_capture:流視頻
-
face_detector:Haar級聯的人臉分類器。我使用了haarcascade_frontalface_alt.xml
-
open_eyes_detector:Haar級聯睜眼分類器。我使用了haarcascade_eye_tree_eyeglasses.xml
-
left_eye_detector:Haar級聯的左眼分類器。我使用了haarcascade_lefteye_2splits.xml,它可以檢測睜眼或閉眼。
-
right_eye_detector:Haar級聯的右眼分類器。我使用了haarcascade_righteye_2splits.xml,它可以檢測睜眼或閉眼。
-
data:已知編碼和已知名稱的字典
-
eyes_detected:包含每個名稱的眼睛狀態歷史記錄的字典。
在第2-4行,我們從網絡攝像頭流中獲取一個幀,然后調整其大小以加快計算速度。
在第10行,我們從幀中檢測人臉,然后在第21行,我們將其編碼為128-d矢量。
在第23-38行,我們將這個向量與已知的人臉編碼進行比較,並通過計算匹配的次數來確定此人的姓名。選擇匹配次數最多的一個。
從第45行開始,我們試着探測眼睛進入人臉框。
首先,我們嘗試用睜眼檢測器來檢測睜眼。如果探測器探測成功,則在第54行,將“1”添加到眼睛狀態歷史記錄中,這意味着眼睛是睜開的,因為睜開的眼睛探測器無法檢測到閉着的眼睛。否則,如果第一個分類器失敗(可能是因為眼睛是閉着的,或者僅僅是因為它不能識別眼睛),則使用左眼和右眼檢測器。人臉被分為左右兩側,以便對各個探測器進行分類。
從第92行開始,提取眼睛部分,訓練后的模型預測眼睛是否閉合。如果檢測到一只眼睛閉着,則兩眼都將被預測為閉着,並將“0”添加到眼睛狀態歷史記錄中。否則就可以斷定眼睛是睜開的。
最后,在第110行,is blinking()函數用於檢測眨眼,如果該人眨眼,則顯示姓名。整個代碼都可以在我的github帳戶上找到。
- 我的github帳戶
使用眨眼檢測功能阻止照片攻擊的演示視頻:
https://youtu.be/arQN6w0fZw8
參考文獻
-
https://docs.opencv.org/3.4.3/d7/d8b/tutorial_py_face_detection.html
-
https://www.pyimagesearch.com/2018/06/18/face-recognition-with-opencv-python-and-deep-learning/
歡迎關注磐創AI博客站:
http://panchuang.net/
sklearn機器學習中文官方文檔:
http://sklearn123.com/
歡迎關注磐創博客資源匯總站:
http://docs.panchuang.net/