目录
fatigue_detecting: 基于图像驾驶员疲劳检测技术研究
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
经过查阅文献,基于人脸表面特征的疲劳检测,主要分为三个部分,打哈欠、眨眼、点头。本实验从人脸朝向、位置、瞳孔朝向、眼睛开合度、眨眼频率、瞳孔收缩率等数据入手,并通过这些数据,实时地计算出驾驶员的注意力集中程度,分析驾驶员是否疲劳驾驶和及时作出安全提示。
一、背景
(1)环境搭建
题主使用的环境配置:python3.9.13+cuda11.3+anaconda3
所需库:
pip install numpy
pip install matplotlib
pip installl imutils
pip install scipy
pip install dlib
其中 dlib下载方法(本文仅提供py3.9版本下载)
首先安装
pip install cmake
pip install boost
下载dlib-19.23.0-cp39-cp39-win_amd64.whl
下载后在对应文件夹下执行(这个大家应该都会吧(我自己是放在环境目录的backages文件夹里))
pip install dlib-19.23.0-cp39-cp39-win_amd64.whl
其他版本
dlib中下载
(2)下载开源数据集
shape_predictor_68_face_landmarks.dat
官方下载地址:Links for dlib
码云下载:fatigue_detecting: 基于图像驾驶员疲劳检测技术研究
(3)Head Pose Estimation 如何理解?
一种比较经典的 Head Pose Estimation 算法的步骤一般为:2D人脸关键点检测;3D人脸模型匹配;求解3D点和对应2D点的转换关系;根据旋转矩阵求解欧拉角。
众所周知一个物体相对于相机的姿态可以使用旋转矩阵和平移矩阵来表示。
平移矩阵:物体相对于相机的空间位置关系矩阵,用T表示;
旋转矩阵:物体相对于相机的空间姿态关系矩阵,用R表示。
于是世界坐标系(UVW)、相机坐标系(XYZ)、图像中心坐标系(uv)和像素坐标系(xy)四兄弟闪亮登场。如果相机完美无瑕,老三可以回家洗洗睡觉,关系也相对简单。
世界坐标系到相机坐标系:
相机坐标系到像素坐标系:
因此像素坐标系和世界坐标系的关系如下:
上式的求解可用DLT(Direct Linear Transform)算法结合最小二乘进行迭代求解,最小二乘的目标函数可为带^的变量为预测值,其余为测量值。
可是相机也很无奈,她不完美,总有点瑕疵,比如径向和切向畸变,那关系就要稍微复杂一些,叫醒阿三继续推导:
相机坐标系要先转换到图像中心坐标系:
然后再被折磨一番(计算考虑畸变):
最后图像中心坐标系到像素坐标系:
看来只要知道世界坐标系内点的位置、像素坐标位置和相机参数就可以搞定旋转和平移矩阵,可上面的关系分明是非线性的,这可怎么解啊?其实OpenCV已经给我们提供了求解PnP问题的函数solvePnp(),一步轻松到位。
得到旋转矩阵后,就可以开心地去见欧拉角了:
世界坐标系中点的位置怎么得到呢?
总不能每时每刻都要测一下人脸各个点在空间的位置
从各种论文中发现,原来大牛们在算法里面内置了一个3D人脸模型,把关键点的空间位置都标出来,就充当真实脸的空间位置;可是大牛又觉得这样不太合理,一个3D人脸模型不能表示所有人的脸,对所有人采用一个模型得到的精度肯定不好,于是便有了3DMM(3D Morphable Model),对不同人可以拟合出对应的3D脸模型,这样关键点的空间位置就比较准确了,Head Pose Estimation 的精度提上去了。
如何确定疲劳?
思路一:可利用姿态估计结果(如Pitch的读数)来判断是否点头及点头幅度
思路二:或用鼻尖处30号点的前后移动值(或是方差,方差表示一个单位时间数据的偏离程度,程度越大,则表示发生点头动作的概率越大、点头幅度越大)
(4)主要代码思路
第一步:2D人脸关键点检测;
第二步:3D人脸模型匹配;
第三步:求解3D点和对应2D点的转换关系;
第四步:根据旋转矩阵求解欧拉角。
# 参考https://github.com/lincolnhard/head-pose-estimation
import cv2
import dlib
import numpy as np
from imutils import face_utils
"""
思路:
第一步:2D人脸关键点检测;第二步:3D人脸模型匹配;
第三步:求解3D点和对应2D点的转换关系;第四步:根据旋转矩阵求解欧拉角。
"""
# 加载人脸检测和姿势估计模型(dlib)
face_landmark_path = 'D:/myworkspace/JupyterNotebook/fatigue_detecting/model/shape_predictor_68_face_landmarks.dat'
"""
只要知道世界坐标系内点的位置、像素坐标位置和相机参数就可以搞定旋转和平移矩阵(OpenCV自带函数solvePnp())
"""
# 世界坐标系(UVW):填写3D参考点,该模型参考http://aifi.isr.uc.pt/Downloads/OpenGL/glAnthropometric3DModel.cpp
object_pts = np.float32([[6.825897, 6.760612, 4.402142], #33左眉左上角
[1.330353, 7.122144, 6.903745], #29左眉右角
[-1.330353, 7.122144, 6.903745], #34右眉左角
[-6.825897, 6.760612, 4.402142], #38右眉右上角
[5.311432, 5.485328, 3.987654], #13左眼左上角
[1.789930, 5.393625, 4.413414], #17左眼右上角
[-1.789930, 5.393625, 4.413414], #25右眼左上角
[-5.311432, 5.485328, 3.987654], #21右眼右上角
[2.005628, 1.409845, 6.165652], #55鼻子左上角
[-2.005628, 1.409845, 6.165652], #49鼻子右上角
[2.774015, -2.080775, 5.048531], #43嘴左上角
[-2.774015, -2.080775, 5.048531],#39嘴右上角
[0.000000, -3.116408, 6.097667], #45嘴中央下角
[0.000000, -7.415691, 4.070434]])#6下巴角
# 相机坐标系(XYZ):添加相机内参
K = [6.5308391993466671e+002, 0.0, 3.1950000000000000e+002,
0.0, 6.5308391993466671e+002, 2.3950000000000000e+002,
0.0, 0.0, 1.0]# 等价于矩阵[fx, 0, cx; 0, fy, cy; 0, 0, 1]
# 图像中心坐标系(uv):相机畸变参数[k1, k2, p1, p2, k3]
D = [7.0834633684407095e-002, 6.9140193737175351e-002, 0.0, 0.0, -1.3073460323689292e+000]
# 像素坐标系(xy):填写凸轮的本征和畸变系数
cam_matrix = np.array(K).reshape(3, 3).astype(np.float32)
dist_coeffs = np.array(D).reshape(5, 1).astype(np.float32)
# 重新投影3D点的世界坐标轴以验证结果姿势
reprojectsrc = np.float32([[10.0, 10.0, 10.0],
[10.0, 10.0, -10.0],
[10.0, -10.0, -10.0],
[10.0, -10.0, 10.0],
[-10.0, 10.0, 10.0],
[-10.0, 10.0, -10.0],
[-10.0, -10.0, -10.0],
[-10.0, -10.0, 10.0]])
# 绘制正方体12轴
line_pairs = [[0, 1], [1, 2], [2, 3], [3, 0],
[4, 5], [5, 6], [6, 7], [7, 4],
[0, 4], [1, 5], [2, 6], [3, 7]]
def get_head_pose(shape):
# 填写2D参考点,注释遵循https://ibug.doc.ic.ac.uk/resources/300-W/
"""
17左眉左上角/21左眉右角/22右眉左上角/26右眉右上角/36左眼左上角/39左眼右上角/42右眼左上角/
45右眼右上角/31鼻子左上角/35鼻子右上角/48左上角/54嘴右上角/57嘴中央下角/8下巴角
"""
# 像素坐标集合
image_pts = np.float32([shape[17], shape[21], shape[22], shape[26], shape[36],
shape[39], shape[42], shape[45], shape[31], shape[35],
shape[48], shape[54], shape[57], shape[8]])
"""
用solvepnp或sovlepnpRansac,输入3d点、2d点、相机内参、相机畸变,输出r、t之后
用projectPoints,输入3d点、相机内参、相机畸变、r、t,输出重投影2d点
计算原2d点和重投影2d点的距离作为重投影误差
"""
# solvePnP计算姿势——求解旋转和平移矩阵:
# rotation_vec表示旋转矩阵,translation_vec表示平移矩阵,cam_matrix与K矩阵对应,dist_coeffs与D矩阵对应。
_, rotation_vec, translation_vec = cv2.solvePnP(object_pts, image_pts, cam_matrix, dist_coeffs)
# projectPoints重新投影误差
reprojectdst, _ = cv2.projectPoints(reprojectsrc, rotation_vec, translation_vec, cam_matrix,dist_coeffs)
reprojectdst = tuple(map(tuple, reprojectdst.reshape(8, 2)))# 以8行2列显示
# 计算欧拉角calc euler angle
# 参考https://docs.opencv.org/2.4/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html#decomposeprojectionmatrix
rotation_mat, _ = cv2.Rodrigues(rotation_vec)#罗德里格斯公式(将旋转矩阵转换为旋转向量)
pose_mat = cv2.hconcat((rotation_mat, translation_vec))# 水平拼接,vconcat垂直拼接
# eulerAngles –可选的三元素矢量,包含三个以度为单位的欧拉旋转角度
_, _, _, _, _, _, euler_angle = cv2.decomposeProjectionMatrix(pose_mat)# 将投影矩阵分解为旋转矩阵和相机矩阵
return reprojectdst, euler_angle
def main():
# return
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("Unable to connect to camera.")
return
# 检测人脸
detector = dlib.get_frontal_face_detector()
# 检测第一个人脸的关键点
predictor = dlib.shape_predictor(face_landmark_path)
while cap.isOpened():
ret, frame = cap.read()
if ret:
face_rects = detector(frame, 0)
if len(face_rects) > 0:
# 循环脸部位置信息,使用predictor(gray, rect)获得脸部特征位置的信息
shape = predictor(frame, face_rects[0])
# 将脸部特征信息转换为数组array的格式
shape = face_utils.shape_to_np(shape)
# 获取头部姿态
reprojectdst, euler_angle = get_head_pose(shape)
pitch = format(euler_angle[0, 0])
yaw = format(euler_angle[1, 0])
roll = format(euler_angle[2, 0])
print('pitch:{}, yaw:{}, roll:{}'.format(pitch, yaw, roll))
# 标出68个特征点
for (x, y) in shape:
cv2.circle(frame, (x, y), 1, (0, 0, 255), -1)
# 绘制正方体12轴
for start, end in line_pairs:
cv2.line(frame, reprojectdst[start], reprojectdst[end], (0, 0, 255))
# 显示角度结果
cv2.putText(frame, "X: " + "{:7.2f}".format(euler_angle[0, 0]), (20, 20), cv2.FONT_HERSHEY_SIMPLEX,0.75, (0, 0, 255), thickness=2)
cv2.putText(frame, "Y: " + "{:7.2f}".format(euler_angle[1, 0]), (20, 50), cv2.FONT_HERSHEY_SIMPLEX,0.75, (0, 0, 255), thickness=2)
cv2.putText(frame, "Z: " + "{:7.2f}".format(euler_angle[2, 0]), (20, 80), cv2.FONT_HERSHEY_SIMPLEX,0.75, (0, 0, 255), thickness=2)
# 按q退出提示
cv2.putText(frame, "Press 'q': Quit", (20, 450),cv2.FONT_HERSHEY_SIMPLEX, 0.7, (84, 255, 159), 2)
# 窗口显示 show with opencv
cv2.imshow("Head_Posture", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 释放摄像头 release camera
cap.release()
# do a bit of cleanup
cv2.destroyAllWindows()
if __name__ == '__main__':
main()
点头如何判断是否是瞌睡?
主要参数:欧拉角
头部姿态判断打瞌睡得到实时头部姿态的旋转角度过后,为头部旋转角度的3个参数Yaw,Pitch和Roll的示意图,驾驶员在打瞌睡时,显然头部会做类似于点头和倾斜的动作.而根据一般人的打瞌睡时表现出来的头部姿态,显然很少会在Yaw上有动作,而主要集中在Pitch和Roll的行为.设定参数阈值为0.3,在一个时间段内10 s内,当I PitchI≥20°或者|Rolll≥20°的时间比例超过0.3时,就认为驾驶员处于打瞌睡的状态,发出预警。