活体检测(点头,摇头,张嘴等动态识别)
某本书里有一句话,等我去读、去拍案。
田间的野老,等我去了解、去惊识。
山风与发,冷泉与舌,
流云与眼,松涛与耳,
他们等着,在神秘的时间的两端等着,
等着相遇的一刹——
一旦相遇,就不一样了,永远不一样了。
—— 张晓风 《我还有一片风景要完成》
背景说明
为什么会写这篇文章,在这里,向大家做一个解释,也作为一个开场白,供大家参考,知道这篇文章会涉及那些内容:
1.对于人脸识别打卡,存在照片打卡,没有活体检测
2.目前的活体检测服务售价高,造成开发成本
3.自主集成度低,不能随意的修改变化,响应业务需求
因此,在这里写下这篇文章,但是,由于活体检测的算法以及核心代码是一位
github
的技术大佬,在这里向前辈致敬!Live_Detection https://github.com/echo1118/Live_Detection
代码实现
from scipy.spatial import distance as dist
from imutils.video import FileVideoStream
from imutils.video import VideoStream
from imutils import face_utils
import argparse
import imutils
import time
import dlib
import cv2
import numpy as np
def eye_aspect_ratio(eye):
# 计算眼睛的两组垂直关键点之间的欧式距离
A = dist.euclidean(eye[1], eye[5]) # 1,5是一组垂直关键点
B = dist.euclidean(eye[2], eye[4]) # 2,4是一组
# 计算眼睛的一组水平关键点之间的欧式距离
C = dist.euclidean(eye[0], eye[3]) # 0,3是一组水平关键点
# 计算眼睛纵横比
ear = (A + B) / (2.0 * C)
# 返回眼睛纵横比
return ear
def mouth_aspect_ratio(mouth):
# 默认二范数:求特征值,然后求最大特征值得算术平方根
A = np.linalg.norm(mouth[2] - mouth[9]) # 51, 59(人脸68个关键点)
B = np.linalg.norm(mouth[4] - mouth[7]) # 53, 57
C = np.linalg.norm(mouth[0] - mouth[6]) # 49, 55
mar = (A + B) / (2.0 * C)
return mar
def nose_jaw_distance(nose, jaw):
# 计算鼻子上一点"27"到左右脸边界的欧式距离
face_left1 = dist.euclidean(nose[0], jaw[0]) # 27, 0
face_right1 = dist.euclidean(nose[0], jaw[16]) # 27, 16
# 计算鼻子上一点"30"到左右脸边界的欧式距离
face_left2 = dist.euclidean(nose[3], jaw[2]) # 30, 2
face_right2 = dist.euclidean(nose[3], jaw[14]) # 30, 14
# 创建元组,用以保存4个欧式距离值
face_distance = (face_left1, face_right1, face_left2, face_right2)
return face_distance
def eyebrow_jaw_distance(leftEyebrow, jaw):
# 计算左眉毛上一点"24"到左右脸边界的欧式距离(镜像对称)
eyebrow_left = dist.euclidean(leftEyebrow[2], jaw[0]) # 24, 0
eyebrow_right = dist.euclidean(leftEyebrow[2], jaw[16]) # 24, 16
# 计算左右脸边界之间的欧式距离
left_right = dist.euclidean(jaw[0], jaw[16]) # 0, 16
# 创建元组,用以保存3个欧式距离值
eyebrow_distance = (eyebrow_left, eyebrow_right, left_right)
return eyebrow_distance
# 构造参数解析并解析参数
def Face_Recognize(file_path):
EYE_AR_THRESH = 0.27 # 眨眼阈值
EYE_AR_CONSEC_FRAMES =2 # 闭眼次数阈值
# 张嘴阈值
MAR_THRESH = 0.5
# 初始化眨眼帧计数器和总眨眼次数
COUNTER_EYE = 0
TOTAL_EYE = 0
# 初始化张嘴帧计数器和总张嘴次数
COUNTER_MOUTH = 0
TOTAL_MOUTH = 0
# 初始化摇头帧计数器和摇头次数
distance_left = 0
distance_right = 0
TOTAL_FACE = 0
# 初始化点头帧计数器和点头次数
nod_flag = 0
TOTAL_NOD = 0
# 初始化dlib的人脸检测器(基于HOG),然后创建面部界标预测器
print("[Prepare000] 加载面部界标预测器...")
# 表示脸部位置检测器
detector = dlib.get_frontal_face_detector()
# 表示脸部特征位置检测器
predictor = dlib.shape_predictor("./static/shape_predictor_68_face_landmarks.dat")
# 左右眼的索引
(lStart, lEnd) = face_utils.FACIAL_LANDMARKS_IDXS["left_eye"]
(rStart, rEnd) = face_utils.FACIAL_LANDMARKS_IDXS["right_eye"]
# 嘴唇的索引
(mStart, mEnd) = face_utils.FACIAL_LANDMARKS_IDXS["mouth"]
# 鼻子的索引
(nStart, nEnd) = face_utils.FACIAL_LANDMARKS_IDXS["nose"]
# 下巴的索引
(jStart, jEnd) = face_utils.FACIAL_LANDMARKS_IDXS['jaw']
# 左眉毛的索引
(Eyebrow_Start, Eyebrow_End) = face_utils.FACIAL_LANDMARKS_IDXS['left_eyebrow']
# 启动视频流线程
print("[Prepare111] 启动视频流线程...")
print("[Prompt information] 按Q键退出...")
vs = FileVideoStream(file_path).start()
fileStream = True
time.sleep(1.0)
# 循环播放视频流中的帧
while True:
# 如果这是一个文件视频流,那么我们需要检查缓冲区中是否还有更多的帧需要处理
if fileStream and not vs.more():
break
# 从线程视频文件流中获取帧,调整大小并将其转换为灰度通道
frame = vs.read()
frame = imutils.resize(frame, width=600)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 在灰度框中检测人脸
rects = detector(gray, 0)
# 循环人脸检测
for rect in rects:
shape = predictor(gray, rect)
shape = face_utils.shape_to_np(shape)
# 提取左眼和右眼坐标,然后使用该坐标计算两只眼睛的眼睛纵横比
leftEye = shape[lStart:lEnd]
rightEye = shape[rStart:rEnd]
leftEAR = eye_aspect_ratio(leftEye)
rightEAR = eye_aspect_ratio(rightEye)
# 提取嘴唇坐标,然后使用该坐标计算嘴唇纵横比
Mouth = shape[mStart:mEnd]
mouthMAR = mouth_aspect_ratio(Mouth)
# 提取鼻子和下巴的坐标,然后使用该坐标计算鼻子到左右脸边界的欧式距离
nose = shape[nStart:nEnd]
jaw = shape[jStart:jEnd]
NOSE_JAW_Distance = nose_jaw_distance(nose, jaw)
# 提取左眉毛的坐标,然后使用该坐标计算左眉毛到左右脸边界的欧式距离
leftEyebrow = shape[Eyebrow_Start:Eyebrow_End]
Eyebrow_JAW_Distance = eyebrow_jaw_distance(leftEyebrow, jaw)
# 对左右两只眼睛的纵横比取平均值
ear = (leftEAR + rightEAR) / 2.0
# 移植嘴唇纵横比
mar = mouthMAR
# 移植鼻子到左右脸边界的欧式距离
face_left1 = NOSE_JAW_Distance[0]
face_right1 = NOSE_JAW_Distance[1]
face_left2 = NOSE_JAW_Distance[2]
face_right2 = NOSE_JAW_Distance[3]
# 移植左眉毛到左右脸边界的欧式距离,及左右脸边界之间的欧式距离
eyebrow_left = Eyebrow_JAW_Distance[0]
eyebrow_right = Eyebrow_JAW_Distance[1]
left_right = Eyebrow_JAW_Distance[2]
# 判断眼睛纵横比是否低于眨眼阈值,如果是,则增加眨眼帧计数器
if ear < EYE_AR_THRESH:
COUNTER_EYE += 1
# 否则,眼睛的纵横比不低于眨眼阈值
else:
# 如果闭上眼睛的次数足够多,则增加眨眼的总次数
if COUNTER_EYE >= EYE_AR_CONSEC_FRAMES:
TOTAL_EYE += 1
# 重置眼框计数器
COUNTER_EYE = 0
# 判断嘴唇纵横比是否高于张嘴阈值,如果是,则增加张嘴帧计数器
if mar > MAR_THRESH:
COUNTER_MOUTH += 1
# 否则,嘴唇的纵横比低于或等于张嘴阈值
else:
# 如果张嘴帧计数器不等于0,则增加张嘴的总次数
if COUNTER_MOUTH != 0:
TOTAL_MOUTH += 1
COUNTER_MOUTH = 0
# 根据鼻子到左右脸边界的欧式距离,判断是否摇头
# 左脸大于右脸
if face_left1 >= face_right1 + 2 and face_left2 >= face_right2 + 2:
distance_left += 1
# 右脸大于左脸
if face_right1 >= face_left1 + 2 and face_right2 >= face_left2 + 2:
distance_right += 1
# 左脸大于右脸,并且右脸大于左脸,判定摇头
if distance_left != 0 and distance_right != 0:
TOTAL_FACE += 1
distance_right = 0
distance_left = 0
# 两边之和是否小于或等于第三边+阈值,来判断是否点头
# 根据左眉毛到左右脸边界的欧式距离与左右脸边界之间的欧式距离作比较,判断是否点头
if eyebrow_left + eyebrow_right <= left_right + 3:
nod_flag += 1
if nod_flag != 0 and eyebrow_left + eyebrow_right >= left_right + 3:
TOTAL_NOD += 1
nod_flag = 0
# 画出画框上眨眼的总次数以及计算出的帧的眼睛纵横比
cv2.putText(frame, "Blinks: {}".format(TOTAL_EYE), (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
# 画出张嘴的总次数以及计算出的帧的嘴唇纵横比
cv2.putText(frame, "Mouth is open: {}".format(TOTAL_MOUTH), (10, 60),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
# 画出摇头次数
cv2.putText(frame, "shake one's head: {}".format(TOTAL_FACE), (10, 90),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
# 画出点头次数
cv2.putText(frame, "nod: {}".format(TOTAL_NOD), (10, 120),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
# 活体检测
cv2.putText(frame, "Live detection: wink(5)", (300, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
if TOTAL_EYE >= 5: # 眨眼五次
cv2.putText(frame, "open your mouth(3)", (300, 60),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
if TOTAL_MOUTH >= 3: # 张嘴三次
cv2.putText(frame, "shake your head(2)", (300, 90),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
if TOTAL_FACE >= 2: # 摇头两次
cv2.putText(frame, "nod(2)", (300, 120),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
if TOTAL_NOD >= 2: # 点头两次
cv2.putText(frame, "Live detection: done", (300, 150),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
# 展示窗口
cv2.imshow("Frame", frame)
key = cv2.waitKey(1) & 0xFF
# Q键退出
if key == ord("q"):
break
# 撕毁窗口
cv2.destroyAllWindows()
vs.stop()
这段Python代码实现了一个基于计算机视觉的人脸动作识别系统,主要用于实时分析视频流中的人脸行为,如眨眼、张嘴、摇头和点头等动作。
-
导入必要的库:
scipy.spatial.distance
用于计算两个点之间的欧氏距离。imutils.video.FileVideoStream
和imutils.video.VideoStream
用于处理视频流。imutils.face_utils
包含一些方便的人脸特征点操作函数。- 其他库包括OpenCV (
cv2
)、NumPy (numpy
) 和时间 (time
) 等,用于图像处理和程序控制。
-
定义了几个辅助函数:
eye_aspect_ratio(eye)
:计算眼睛的纵横比(EAR),这是评估眼睛是否闭合的关键指标。mouth_aspect_ratio(mouth)
:计算嘴巴的纵横比(MAR),用来判断嘴巴是否张开。nose_jaw_distance(nose, jaw)
:计算鼻子到左右脸颊边界的距离,用于估计头部运动。eyebrow_jaw_distance(leftEyebrow, jaw)
:计算眉毛到左右脸颊边界的距离,辅助判断点头动作。
-
主函数
Face_Recognize(file_path)
接收一个视频文件路径作为输入:- 设置了几个阈值变量,如眨眼、张嘴的阈值等。
- 使用dlib库加载预训练的人脸检测器和面部特征预测器模型。
- 获取人脸特征点的索引范围(例如眼睛、嘴巴、鼻子、下巴和眉毛)。
-
开启视频流,读取每一帧图像并执行以下步骤:
- 对每帧图像进行灰度处理,并使用dlib检测人脸。
- 遍历检测到的人脸,利用面部特征预测器提取68个特征点。
- 计算眼睛和嘴巴的纵横比以及其他几何特征。
- 根据预先设定的阈值,累计眨眼和张嘴的动作次数。
- 利用鼻子和下巴的位置变化判断摇头动作。
- 根据眉毛位置判断点头动作。
-
将检测到的动作次数实时显示在视频帧上,并通过键盘事件监听用户按键,当按下
q
时退出程序。
这就是主要方法,在原来的方法上进行了修改,取消了摄像头捕获视频,采用API接口调用传参,更加灵活!但是,对于传参方式以及效率问题,目前也是在进行思考,是否采用实时视频流进行传输还是怎么弄,目前没有确定,现在,我们这里就是给大家一个基础的核心解决方法!就是前端传递视频地址到后端,后端对于视频画像进行解析,以此进行活体判断,当然,必然存在不足,目前本人技术尚浅,后期再做深入!
API接口
import base64
from flask import Flask, request
import numpy as np
import cv2
import imutils
import dlib
from imutils import face_utils
import DynamicRecognition
app = Flask(__name__)
@app.route('/process_video', methods=['GET'])
def process_video():
# 从前端获取视频文件地址
file_path = request.args.get('file_path')
# 视频分析
DynamicRecognition.Face_Recognize(file_path)
# 返回处理后的结果(可以是JSON格式)
return {"result": "Processed successfully"}
if __name__ == '__main__':
app.run(debug=True)