1. 系统概述
疲劳驾驶检测系统是一个基于计算机视觉和机器学习技术的应用程序,旨在实时监测驾驶员的疲劳状态,通过分析面部特征和头部姿态来判断驾驶员是否处于疲劳状态,并在检测到疲劳迹象时发出警告。本系统采用 PyQt5 构建图形用户界面,结合 OpenCV 进行图像处理,使用特征脸 (Eigenfaces) 算法进行驾驶员识别,并通过多种疲劳指标计算实现疲劳状态检测。
1.1 系统功能
- 实时视频捕获与显示
- 驾驶员面部特征检测与关键点定位
- 驾驶员身份识别
- 头部姿态估计(俯仰角、偏航角、滚转角)
- 眼睛状态检测(眨眼频率)
- 嘴巴状态检测(哈欠频率)
- 头部运动检测(点头频率)
- 疲劳状态综合评估与警告
- 驾驶时间记录与超时警告
1.2 技术架构
系统采用模块化设计,主要由以下几个部分组成:
- 图形用户界面 (GUI):基于 PyQt5 构建,提供用户交互界面
- 视频处理线程:独立线程处理视频流,避免 UI 卡顿
- 面部特征检测模块:使用 dlib 库进行人脸检测和关键点定位
- 头部姿态估计模块:计算驾驶员头部的三维姿态
- 疲劳指标计算模块:计算 EAR、MAR 等疲劳相关指标
- 驾驶员识别模块:使用特征脸算法进行驾驶员身份识别
- 疲劳状态评估模块:综合各项指标评估疲劳状态
2. 技术细节与实现原理
2.1 开发环境与依赖库
系统基于 Python 开发,主要依赖以下库:
- OpenCV:用于图像处理、视频捕获和基本计算机视觉操作
- imutils:OpenCV 的实用工具包,提供图像缩放等功能
- dlib:包含先进的机器学习算法,用于人脸检测和关键点定位
- PyQt5:用于构建图形用户界面
- numpy:用于数值计算
- datetime:用于时间相关操作
2.2 系统核心类与模块
2.2.1 VideoThread 类 - 视频处理与疲劳检测核心
VideoThread 类是系统的核心处理单元,继承自 QThread,在独立线程中运行,负责视频捕获、图像处理和疲劳状态检测。
2.2.1.1 初始化参数与状态变量
def __init__(self):
super().__init__()
self.running = False # 线程运行标志
self.face_path = './face_path' # 人脸数据存储路径
self.path = 0 # 使用摄像头(0表示默认摄像头)
# 疲劳检测阈值
self.EAR_threshold = 0.18 # 眼睛纵横比阈值
self.MAR_threshold = 0.65 # 嘴巴纵横比阈值
self.pitch_threshold = 8.0 # 点头检测阈值
self.Driving_Time_Threshold = 1800 # 驾驶时间阈值(30分钟)
# 时间相关变量
self.driving_time = 0 # 累计驾驶时间
self.starttime = datetime.datetime.now() # 驾驶开始时间
# 疲劳指标计数器
self.blink_counter = 0 # 眨眼计数(临时)
self.blinks = 0 # 眨眼计数(累计)
self.yawn_counter = 0 # 哈欠计数(临时)
self.yawns = 0 # 哈欠计数(累计)
self.nod_counter = 0 # 点头计数(临时)
self.nods = 0 # 点头计数(累计)
self.P80_sum_time1 = [] # PERCOLS计算时间累计1
self.P80_sum_time2 = [] # PERCOLS计算时间累计2
self.f = 0 # PERCOLS值(疲劳程度指标)
# 状态标志
self.alarm_flag = '正常' # 警报状态
self.last_params = [] # 上次识别的驾驶员参数
self.eyes_closed = False # 眼睛闭合状态
self.mouth_open = False # 嘴巴张开状态
self.head_nodding = False # 头部点头状态
# 时间记录
self.blink_start_time = None # 眨眼开始时间
self.yawn_start_time = None # 哈欠开始时间
self.nod_start_time = None # 点头开始时间
self.P80_start_time1 = None # PERCOLS时间记录1
self.P80_start_time2 = None # PERCOLS时间记录2
# 图表数据
self.EAR_history = [] # EAR历史数据
self.MAR_history = [] # MAR历史数据
self.max_history_length = 60 # 历史数据最大长度
# 3D姿态绘制参数
self.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]]
# 加载EAR和MAR标准值
self.everybody_EAR_mean, self.everybody_EAR_min, _ = get_everybody_EARandMAR_standard(self.face_path)
这些参数和变量用于跟踪驾驶员状态、记录时间信息、存储疲劳指标,并设置各种检测阈值。系统会加载预先计算的 EAR (眼睛纵横比) 和 MAR (嘴巴纵横比) 标准值,用于个性化疲劳检测。
2.2.1.2 run 方法 - 核心处理逻辑
run 方法是线程的入口点,包含整个系统的核心处理逻辑:
- 视频捕获与预处理
- 人脸检测与关键点定位
- 驾驶员识别
- 头部姿态估计
- 眼睛和嘴巴状态分析
- 疲劳指标计算
- 疲劳状态评估
- 结果可视化
def run(self):
self.cap = cv2.VideoCapture(self.path)
while self.running:
ret, im_rd = self.cap.read()
if ret:
im_rd = imutils.resize(im_rd, height=480, width=640) # 调整图像大小
original_img = im_rd.copy()
# 灰度化处理
img_gray = cv2.cvtColor(original_img, cv2.COLOR_BGR2GRAY)
# 人脸检测
faces = detector(img_gray, 0)
# 初始化信息字典
info = {
'alarm': self.alarm_flag,
'driver_id': '未知',
'driving_time': 0,
'pitch': 0,
'yaw': 0,
'roll': 0,
'ear': 0,
'mar': 0,
'eyes_state': '睁开',
'nod_duration': 0,
'yawn_duration': 0,
'blinks': 0,
'yawns': 0,
'nods': 0,
'percols': 0
}
# 处理检测到的人脸
if len(faces) == 1:
for k, d in enumerate(faces):
try:
# 提取人脸区域并调整大小
roi_gray = img_gray[d.top():d.bottom(), d.left():d.right()]
roi_gray = cv2.resize(roi_gray, (92, 112))
# 驾驶员识别
params = Eigen_Face_Model.predict(roi_gray)
except:
continue
# 获取面部关键点
shape = predictor(original_img, d)
shape_array = face_utils.shape_to_np(shape)
# 驾驶员变更检测
if params[0] != self.last_params:
self.driving_time = 0
self.starttime = datetime.datetime.now()
self.last_params = params[0]
# 尝试获取驾驶员ID
try:
info['driver_id'] = names[params[0]]
except:
info['driver_id'] = '未识别'
# 头部姿态估计
reprojectdst, _, pitch, roll, yaw = HPE.get_head_pose(shape_array)
info['pitch'] = round(pitch, 2)
info['yaw'] = round(yaw, 2)
info['roll'] = round(roll, 2)
# 提取面部特征点
leftEye = shape_array[lStart:lEnd]
rightEye = shape_array[rStart:rEnd]
mouth = shape_array[mStart:mEnd]
# 计算眼睛纵横比(EAR)
leftEAR = ARE.eye_aspect_ratio(leftEye)
rightEAR = ARE.eye_aspect_ratio(rightEye)
EAR = (leftEAR + rightEAR) / 2.0
info['ear'] = round(EAR, 2)
# 眨眼检测
current_time = datetime.datetime.now()
if EAR < self.EAR_threshold and not self.eyes_closed:
self.eyes_closed = True
self.blink_start_time = current_time
self.blink_counter += 1
elif EAR >= self.EAR_threshold and self.eyes_closed:
self.eyes_closed = False
if self.blink_start_time and (current_time - self.blink_start_time).total_seconds() < 0.5:
self.blinks += 1
self.blink_start_time = None
# 计算嘴巴纵横比(MAR)
MAR = ARE.mouth_aspect_ratio(mouth)
info['mar'] = round(MAR, 2)
# 哈欠检测
if MAR > self.MAR_threshold and not self.mouth_open:
self.mouth_open = True
self.yawn_start_time = current_time
self.yawn_counter += 1
elif MAR <= self.MAR_threshold and self.mouth_open:
self.mouth_open = False
if self.yawn_start_time:
yawn_duration = (current_time - self.yawn_start_time).total_seconds()
if yawn_duration >= 1.0:
self.yawns += 1
info['yawn_duration'] = int(yawn_duration)
self.yawn_start_time = None
# 点头检测
if pitch > self.pitch_threshold and not self.head_nodding:
self.head_nodding = True
self.nod_start_time = current_time
self.nod_counter += 1
elif pitch <= self.pitch_threshold and self.head_nodding:
self.head_nodding = False
if self.nod_start_time:
nod_duration = (current_time - self.nod_start_time).total_seconds()
if nod_duration >= 0.5:
self.nods += 1
info['nod_duration'] = int(nod_duration)
self.nod_start_time = None
# PERCOLS值计算(疲劳程度指标)
if params[0] in range(len(self.everybody_EAR_mean)):
T1 = self.everybody_EAR_min[params[0]] + 0.2 * (
self.everybody_EAR_mean[params[0]] - self.everybody_EAR_min[params[0]])
T2 = self.everybody_EAR_min[params[0]] + 0.8 * (
self.everybody_EAR_mean[params[0]] - self.everybody_EAR_min[params[0]])
# 计算眼睛闭合程度
if EAR < T1 and abs(pitch) < 15 and abs(yaw) < 25 and abs(roll) < 15:
if self.P80_start_time1 is None:
self.P80_start_time1 = current_time
elif self.P80_start_time1 is not None:
duration = (current_time - self.P80_start_time1).total_seconds()
if duration > 0:
self.P80_sum_time1.append(duration)
self.P80_start_time1 = None
if EAR < T2 and abs(pitch) < 15 and abs(yaw) < 25 and abs(roll) < 15:
if self.P80_start_time2 is None:
self.P80_start_time2 = current_time
elif self.P80_start_time2 is not None:
duration = (current_time - self.P80_start_time2).total_seconds()
if duration > 0:
self.P80_sum_time2.append(duration)
self.P80_start_time2 = None
# 计算PERCOLS值
sum_t1 = sum(self.P80_sum_time1)
sum_t2 = sum(self.P80_sum_time2)
if sum_t2 > 0:
self.f = min(round(sum_t1 / sum_t2, 2), 1.0)
else:
self.f = 0
# 每60秒重置PERCOLS计算
if int(self.driving_time) % 60 == 0:
self.P80_sum_time1 = []
self.P80_sum_time2 = []
self.f = 0
# 设置眼睛状态
info['eyes_state'] = '闭合' if EAR < self.EAR_threshold else '睁开'
info['blinks'] = self.blinks
info['yawns'] = self.yawns
info['nods'] = self.nods
info['percols'] = self.f
# 疲劳状态判断
if int(self.driving_time) % 60 == 0:
if self.blinks >= 30:
self.alarm_flag = '眨眼频率警告'
elif self.yawns >= 5:
self.alarm_flag = '哈欠频率警告'
elif self.nods >= 3:
self.alarm_flag = '点头频率警告'
elif self.f > 0.6:
self.alarm_flag = 'PERCOLS值警告'
elif self.driving_time > self.Driving_Time_Threshold:
self.alarm_flag = '长时间驾驶警告'
else:
self.alarm_flag = '正常'
# 重置计数器
self.blinks = 0
self.yawns = 0
self.nods = 0
info['alarm'] = self.alarm_flag
info['driving_time'] = int(self.driving_time)
# 可视化处理结果
for i in range(68):
cv2.circle(im_rd, (shape.part(i).x, shape.part(i).y), 2, (0, 255, 0), -1, 8)
for start, end in self.line_pairs:
cv2.line(im_rd, (int(reprojectdst[start][0]), int(reprojectdst[start][1])),
(int(reprojectdst[end][0]), int(reprojectdst[end][1])), (0, 0, 255))
# 绘制眼睛和嘴巴轮廓
leftEyeHull = cv2.convexHull(leftEye)
rightEyeHull = cv2.convexHull(rightEye)
cv2.drawContours(im_rd, [leftEyeHull], -1, (0, 255, 0), 1)
cv2.drawContours(im_rd, [rightEyeHull], -1, (0, 255, 0), 1)
mouthHull = cv2.convexHull(mouth)
cv2.drawContours(im_rd, [mouthHull], -1, (0, 255, 0), 1)
# 添加状态文字
status_color = (0, 255, 0) if self.alarm_flag == '正常' else (0, 0, 255)
cv2.putText(im_rd, f"状态: {self.alarm_flag}", (10, 50),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2)
elif len(faces) == 0:
cv2.putText(im_rd, "未检测到人脸", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
else:
cv2.putText(im_rd, "检测到多个人脸", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)
# 更新驾驶时间
endtime = datetime.datetime.now()
self.driving_time = (endtime - self.starttime).total_seconds()
# 转换图像用于Qt显示
rgb_image = cv2.cvtColor(im_rd, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_image.shape
bytes_per_line = ch * w
qt_image = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
self.change_pixmap_signal.emit(qt_image)
# 发送信息更新UI
self.update_info_signal.emit(info)
else:
# 视频结束或摄像头断开,重新打开
self.reset_counters()
self.cap = cv2.VideoCapture(self.path)
2.2.2 FatigueDetectionApp 类 - 图形用户界面
FatigueDetectionApp 类继承自 QMainWindow,负责构建和管理图形用户界面,提供用户交互功能。
2.2.2.1 界面布局设计
界面采用左右分栏布局,左侧为视频显示区域,右侧为驾驶状态监测信息:
def __init__(self):
super().__init__()
self.setWindowTitle('疲劳驾驶检测系统')
self.setMinimumSize(1200, 700) # 增大窗口尺寸
# 设置样式表,提高界面美观度和对比度
self.setStyleSheet("""
QMainWindow {
background-color: #121212;
}
QLabel {
color: #e0e0e0;
font-size: 14px;
}
QPushButton {
background-color: #3b82f6;
color: white;
border-radius: 5px;
padding: 8px 15px;
font-size: 14px;
}
QPushButton:hover {
background-color: #2563eb;
}
QPushButton:pressed {
background-color: #1d4ed8;
}
QGroupBox {
border: 2px solid #3b82f6;
border-radius: 8px;
margin-top: 10px;
padding-top: 15px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 10px;
color: #3b82f6;
font-weight: bold;
font-size: 15px;
}
""")
# 创建中央部件和主布局
central_widget = QWidget()
main_layout = QHBoxLayout(central_widget)
main_layout.setSpacing(15) # 增加间距
# 左侧视频显示区域
video_group = QGroupBox("实时视频")
video_group.setFont(title_font)
video_layout = QVBoxLayout()
self.video_label = QLabel("等待视频输入...")
self.video_label.setAlignment(Qt.AlignCenter)
self.video_label.setMinimumSize(700, 500)
self.video_label.setStyleSheet("border: 2px solid #3b82f6; border-radius: 5px;")
video_layout.addWidget(self.video_label)
video_group.setLayout(video_layout)
# 右侧信息显示区域
info_group = QGroupBox("驾驶状态监测")
info_group.setFont(title_font)
info_layout = QVBoxLayout()
info_layout.setSpacing(10) # 增加间距
# 驾驶人信息
driver_info_layout = QHBoxLayout()
self.driver_id_label = QLabel("驾驶人: 未识别")
self.driver_id_label.setFont(font)
self.driving_time_label = QLabel("驾驶时间: 0 秒")
self.driving_time_label.setFont(font)
driver_info_layout.addWidget(self.driver_id_label)
driver_info_layout.addWidget(self.driving_time_label)
info_layout.addLayout(driver_info_layout)
# 警告信息 - 增大尺寸和对比度
self.alarm_label = QLabel("状态: 正常")
self.alarm_label.setFont(alarm_font)
self.alarm_label.setStyleSheet("color: #4caf50; font-size: 18px; font-weight: bold;")
self.alarm_label.setAlignment(Qt.AlignCenter)
self.alarm_label.setMinimumHeight(40)
info_layout.addWidget(self.alarm_label)
# 头部姿态
pose_group = QGroupBox("头部姿态")
pose_group.setFont(font)
pose_layout = QHBoxLayout()
self.pitch_label = QLabel("俯仰角(Pitch): 0.00°")
self.pitch_label.setFont(font)
self.yaw_label = QLabel("偏航角(Yaw): 0.00°")
self.yaw_label.setFont(font)
self.roll_label = QLabel("滚转角(Roll): 0.00°")
self.roll_label.setFont(font)
pose_layout.addWidget(self.pitch_label)
pose_layout.addWidget(self.yaw_label)
pose_layout.addWidget(self.roll_label)
pose_group.setLayout(pose_layout)
info_layout.addWidget(pose_group)
# 面部特征
face_group = QGroupBox("面部特征")
face_group.setFont(font)
face_layout = QHBoxLayout()
self.ear_label = QLabel("眼睛纵横比(EAR): 0.00")
self.ear_label.setFont(font)
self.mar_label = QLabel("嘴巴纵横比(MAR): 0.00")
self.mar_label.setFont(font)
self.eyes_state_label = QLabel("眼睛状态: 睁开")
self.eyes_state_label.setFont(font)
face_layout.addWidget(self.ear_label)
face_layout.addWidget(self.mar_label)
face_layout.addWidget(self.eyes_state_label)
face_group.setLayout(face_layout)
info_layout.addWidget(face_group)
# 疲劳指标
fatigue_group = QGroupBox("疲劳指标")
fatigue_group.setFont(font)
fatigue_layout = QGridLayout()
fatigue_layout.setSpacing(10) # 增加间距
# 使用更清晰的标签
self.blinks_label = QLabel("0 次")
self.yawns_label = QLabel("0 次")
self.nods_label = QLabel("0 次")
self.percols_label = QLabel("0.00")
self.nod_duration_label = QLabel("0 秒")
self.yawn_duration_label = QLabel("0 秒")
# 设置标签字体
labels = [self.blinks_label, self.yawns_label, self.nods_label,
self.percols_label, self.nod_duration_label, self.yawn_duration_label]
for label in labels:
label.setFont(font)
# 优化布局
fatigue_layout.addWidget(QLabel("眨眼次数:", font=font), 0, 0)
fatigue_layout.addWidget(self.blinks_label, 0, 1)
fatigue_layout.addWidget(QLabel("哈欠次数:", font=font), 1, 0)
fatigue_layout.addWidget(self.yawns_label, 1, 1)
fatigue_layout.addWidget(QLabel("点头次数:", font=font), 2, 0)
fatigue_layout.addWidget(self.nods_label, 2, 1)
fatigue_layout.addWidget(QLabel("PERCOLS值:", font=font), 3, 0)
fatigue_layout.addWidget(self.percols_label, 3, 1)
fatigue_layout.addWidget(QLabel("点头持续时间:", font=font), 0, 2)
fatigue_layout.addWidget(self.nod_duration_label, 0, 3)
fatigue_layout.addWidget(QLabel("哈欠持续时间:", font=font), 1, 2)
fatigue_layout.addWidget(self.yawn_duration_label, 1, 3)
fatigue_group.setLayout(fatigue_layout)
info_layout.addWidget(fatigue_group)
# 控制按钮
control_layout = QHBoxLayout()
self.start_button = QPushButton("开始检测")
self.start_button.setFont(font)
self.start_button.setMinimumHeight(40)
self.stop_button = QPushButton("停止检测")
self.stop_button.setFont(font)
self.stop_button.setMinimumHeight(40)
self.stop_button.setEnabled(False)
control_layout.addWidget(self.start_button)
control_layout.addWidget(self.stop_button)
info_layout.addLayout(control_layout)
info_group.setLayout(info_layout)
# 将视频和信息区域添加到主布局
main_layout.addWidget(video_group)
main_layout.addWidget(info_group)
main_layout.setStretch(0, 3) # 视频区域占3份
main_layout.setStretch(1, 2) # 信息区域占2份
self.setCentralWidget(central_widget)
界面设计考虑了用户体验,采用分组布局清晰展示各类信息,并使用颜色区分正常状态和警告状态,提高视觉对比度。
2.2.2.2 信号与槽机制
PyQt5 的信号与槽机制是界面与后台处理通信的关键:
# 连接信号和槽
self.start_button.clicked.connect(self.start_detection)
self.stop_button.clicked.connect(self.stop_detection)
# 创建视频线程
self.video_thread = VideoThread()
self.video_thread.change_pixmap_signal.connect(self.update_video_frame)
self.video_thread.update_info_signal.connect(self.update_info_display)
- 按钮点击信号连接到相应的槽函数,控制检测的开始和停止
- 视频线程通过信号发送处理后的图像和状态信息,更新 UI 显示
2.3 疲劳检测算法原理
2.3.1 眼睛纵横比 (EAR) 与眨眼检测
眼睛纵横比 (Eye Aspect Ratio, EAR) 是衡量眼睛睁开程度的重要指标,计算公式如下:
其中P1到P6是眼睛周围 6 个关键点的坐标。当眼睛闭合时,EAR 值会显著减小,通常小于 0.2。系统通过监测 EAR 值低于阈值的持续时间来检测眨眼行为。
# 计算眼睛纵横比
def eye_aspect_ratio(eye):
# 计算垂直距离
A = dist.euclidean(eye[1], eye[5])
B = dist.euclidean(eye[2], eye[4])
# 计算水平距离
C = dist.euclidean(eye[0], eye[3])
# 计算EAR
ear = (A + B) / (2.0 * C)
return ear
眨眼检测逻辑:当 EAR 低于阈值且眼睛之前处于睁开状态时,记录眨眼开始时间;当 EAR 高于阈值且眼睛之前处于闭合状态时,计算眨眼持续时间,若持续时间在合理范围内 (小于 0.5 秒),则计数一次眨眼。
2.3.2 嘴巴纵横比 (MAR) 与哈欠检测
嘴巴纵横比 (Mouth Aspect Ratio, MAR) 用于衡量嘴巴张开程度,计算公式如下:
其中P0到P7是嘴巴周围 8 个关键点的坐标。当嘴巴张开 (如打哈欠) 时,MAR 值会显著增大,通常大于 0.6。
# 计算嘴巴纵横比
def mouth_aspect_ratio(mouth):
# 计算垂直距离
A = dist.euclidean(mouth[13], mouth[19])
B = dist.euclidean(mouth[14], mouth[18])
C = dist.euclidean(mouth[15], mouth[17])
# 计算水平距离
D = dist.euclidean(mouth[0], mouth[6])
# 计算MAR
mar = (A + B + C) / (2.0 * D)
return mar
哈欠检测逻辑:当 MAR 高于阈值且嘴巴之前处于闭合状态时,记录哈欠开始时间;当 MAR 低于阈值且嘴巴之前处于张开状态时,计算哈欠持续时间,若持续时间超过 1 秒,则计数一次哈欠。
2.3.3 头部姿态估计
头部姿态估计通过计算头部的三个欧拉角 (俯仰角 Pitch、偏航角 Yaw、滚转角 Roll) 来判断驾驶员的头部运动:
- 俯仰角 (Pitch):头部上下转动
- 偏航角 (Yaw):头部左右转动
- 滚转角 (Roll):头部倾斜
系统使用 68 个人脸关键点,通过解决 PnP (Perspective-n-Point) 问题来估计头部姿态:
# 头部姿态估计
def get_head_pose(shape):
# 3D模型点
model_points = np.array([
(0.0, 0.0, 0.0), # 鼻尖
(0.0, -330.0, -65.0), # 下巴
(-225.0, 170.0, -135.0), # 左眼左角
(225.0, 170.0, -135.0), # 右眼右角
(-150.0, -150.0, -125.0), # 左嘴角
(150.0, -150.0, -125.0) # 右嘴角
])
# 2D图像点
image_points = np.array([
shape[30], # 鼻尖
shape[8], # 下巴
shape[36], # 左眼左角
shape[45], # 右眼右角
shape[48], # 左嘴角
shape[54] # 右嘴角
], dtype="double")
# 相机内参
focal_length = 1.0 * 640
center = (320, 240)
camera_matrix = np.array(
[[focal_length, 0, center[0]],
[0, focal_length, center[1]],
[0, 0, 1]], dtype="double"
)
# 相机畸变参数
dist_coeffs = np.zeros((4, 1))
# 求解PnP问题
success, rotation_vector, translation_vector = cv2.solvePnP(
model_points, image_points, camera_matrix, dist_coeffs,
flags=cv2.SOLVEPNP_ITERATIVE
)
# 计算欧拉角
rotation_matrix, _ = cv2.Rodrigues(rotation_vector)
pose_mat = np.hstack((rotation_matrix, translation_vector))
_, _, _, _, pitch, yaw, roll = cv2.decomposeProjectionMatrix(pose_mat)
# 计算3D模型点的投影,用于可视化
reprojectdst, _ = cv2.projectPoints(
np.array([
(0.0, 0.0, 1000.0),
(0.0, 0.0, 0.0),
(0.0, -330.0, -65.0),
(-225.0, 170.0, -135.0),
(225.0, 170.0, -135.0),
(-150.0, -150.0, -125.0),
(150.0, -150.0, -125.0)
]),
rotation_vector, translation_vector, camera_matrix, dist_coeffs
)
return reprojectdst, rotation_vector, pitch[0], yaw[0], roll[0]
点头检测逻辑:当俯仰角 (Pitch) 大于阈值且头部之前处于非点头状态时,记录点头开始时间;当俯仰角小于阈值且头部之前处于点头状态时,计算点头持续时间,若持续时间超过 0.5 秒,则计数一次点头。
2.3.4 PERCOLS 值 - 疲劳程度综合指标
PERCOLS 值 (Percentage of Eye Closure over a Long Period of Time) 是衡量驾驶员疲劳程度的综合指标,计算公式如下:
其中T1是眼睛闭合程度超过 20% 阈值的时间,T2是眼睛闭合程度超过 80% 阈值的时间。系统通过计算这两个时间段的比值来评估疲劳程度,值越大表示疲劳程度越高。
# PERCOLS值计算
if params[0] in range(len(self.everybody_EAR_mean)):
T1 = self.everybody_EAR_min[params[0]] + 0.2 * (
self.everybody_EAR_mean[params[0]] - self.everybody_EAR_min[params[0]])
T2 = self.everybody_EAR_min[params[0]] + 0.8 * (
self.everybody_EAR_mean[params[0]] - self.everybody_EAR_min[params[0]])
# 计算眼睛闭合程度
if EAR < T1 and abs(pitch) < 15 and abs(yaw) < 25 and abs(roll) < 15:
if self.P80_start_time1 is None:
self.P80_start_time1 = current_time
elif self.P80_start_time1 is not None:
duration = (current_time - self.P80_start_time1).total_seconds()
if duration > 0:
self.P80_sum_time1.append(duration)
self.P80_start_time1 = None
if EAR < T2 and abs(pitch) < 15 and abs(yaw) < 25 and abs(roll) < 15:
if self.P80_start_time2 is None:
self.P80_start_time2 = current_time
elif self.P80_start_time2 is not None:
duration = (current_time - self.P80_start_time2).total_seconds()
if duration > 0:
self.P80_sum_time2.append(duration)
self.P80_start_time2 = None
# 计算PERCOLS值
sum_t1 = sum(self.P80_sum_time1)
sum_t2 = sum(self.P80_sum_time2)
if sum_t2 > 0:
self.f = min(round(sum_t1 / sum_t2, 2), 1.0)
else:
self.f = 0
# 每60秒重置PERCOLS计算
if int(self.driving_time) % 60 == 0:
self.P80_sum_time1 = []
self.P80_sum_time2 = []
self.f = 0
2.4 驾驶员识别 - 特征脸 (Eigenfaces) 算法
系统使用特征脸算法进行驾驶员身份识别,这是一种基于主成分分析 (PCA) 的人脸识别方法:
- 将人脸图像转换为向量
- 使用 PCA 降维,提取主要特征 (特征脸)
- 将新的人脸图像投影到特征脸空间
- 通过计算与训练样本的距离进行识别
# 特征脸识别
from Eigen_Face_Recognizer import *
# 预测驾驶员身份
params = Eigen_Face_Model.predict(roi_gray)
try:
info['driver_id'] = names[params[0]]
except:
info['driver_id'] = '未识别'
特征脸算法的优点是计算效率高,适合实时系统,但在表情变化大、光照条件变化时识别准确率会下降。系统会在驾驶员变更时重置驾驶时间计数。
2.5 多线程处理机制
系统采用多线程架构,将视频处理和 UI 更新分离到不同线程,避免 UI 卡顿:
- VideoThread 线程负责视频捕获、图像处理和疲劳检测
- 主线程负责 UI 界面显示和用户交互
通过 PyQt5 的信号与槽机制实现线程间通信,确保线程安全:
这种设计模式确保了系统的响应性和稳定性,即使视频处理较为耗时,UI 仍然可以保持流畅。
3. 系统工作流程
3.1 启动流程
- 应用程序初始化,创建 FatigueDetectionApp 实例
- 界面初始化,设置布局和控件
- 连接信号与槽
- 用户点击 "开始检测" 按钮
- 启动 VideoThread 线程,开始视频捕获和处理
3.2 处理流程
- 视频线程捕获一帧图像
- 图像预处理:调整大小、灰度化
- 人脸检测,获取人脸区域
- 驾驶员识别,获取驾驶员 ID
- 面部关键点定位,获取 68 个关键点坐标
- 头部姿态估计,计算俯仰角、偏航角、滚转角
- 计算 EAR 和 MAR,检测眨眼和哈欠
- 计算 PERCOLS 值,评估疲劳程度
- 综合各项指标,判断疲劳状态
- 可视化处理结果,绘制关键点和姿态
- 发送处理结果到 UI 线程更新显示
3.3 关闭流程
- 用户点击 "停止检测" 按钮或关闭窗口
- 停止视频线程,释放资源
- 退出应用程序
运行数据:
正在加载dlib模型: D:\Develop_Code\py_work\Fatigue-Driving-Detection-Based-on-Dlib-master\shape_predictor_5_face_landmarks.dat
检测到 OpenCV 版本: 4.0.0
OpenCV contrib 模块已正确安装
/*/*/*/*/*/*/* 特征脸识别器正在训练 /*/*/*/*/*/*/*
成功加载 158 张训练图像,共 2 个人
-*-*-*-*-*-*-* 特征脸识别器训练完成并保存至 ./eigen_face_model.yml -*-*-*-*-*-*-*
所有人睁眼ER平均值: [0.38413600756917976, 0.3819993541503343]
所有人闭眼EAR最小值: [0.2629860803947992, 0.292137046814521]
------------------- 开始执行主函数 -------------------
小于20%的时间: 0 s
小于80%的时间: 0 s
当前驾驶人的EAR: 0.3035419281974605
T1: 0.2872160658296753 ; t2: 0.35990602213430367
小于20%的时间: 0 s
小于80%的时间: 0 s
当前驾驶人的EAR: 0.3069549345872935
T1: 0.2872160658296753 ; t2: 0.35990602213430367
小于20%的时间: 0 s
小于80%的时间: 0 s
当前驾驶人的EAR: 0.2949592314589621
T1: 0.2872160658296753 ; t2: 0.35990602213430367
小于20%的时间: 0 s
小于80%的时间: 0 s
当前驾驶人的EAR: 0.3027418010270625
T1: 0.2872160658296753 ; t2: 0.35990602213430367
小于20%的时间: 0 s
小于80%的时间: 0 s
当前驾驶人的EAR: 0.2902889940735921
T1: 0.2872160658296753 ; t2: 0.35990602213430367
小于20%的时间: 0 s
小于80%的时间: 0 s
当前驾驶人的EAR: 0.29684223252664194
T1: 0.2872160658296753 ; t2: 0.35990602213430367
小于20%的时间: 0 s
小于80%的时间: 0 s
当前驾驶人的EAR: 0.30226910858134476
T1: 0.2872160658296753 ; t2: 0.35990602213430367
小于20%的时间: 0 s
小于80%的时间: 0 s
当前驾驶人的EAR: 0.31337795439975213
T1: 0.2872160658296753 ; t2: 0.35990602213430367
小于20%的时间: 0 s
小于80%的时间: 0 s
当前驾驶人的EAR: 0.31947443683898324
4.项目说明:
#项目文件说明
***************************************************************************************************************
如何运行该项目
在运行项目之前, 应确保你有用于测试的视频文件. 本项目中提供了一个视频例程(driving.mp4)
必须执行: 首先运行 drivers_img_acquire.py 文件, 输入当前驾驶人的名字英文缩写,获取不同驾驶人的两类图像
获取的第一类图像为 摄像头全景图像, 默认存放于 './capture_path/{your name}'. 注意: 要删除capture_path文件夹下面的txt文件.
获取的第二类图像为 驾驶人人脸区域图像, 默认存放于 './face_path/{your name}'. 注意: 要删除face_path文件夹下面的txt文件.
其次运行main.py程序即可.
***************************************************************************************************************
capture_path: 所有驾驶人的全景图像 (仅采集, 未使用)
face_path: 所有驾驶人的人脸区域图像, 用于身份识别的训练
test_video: 测试视频所存放的文件夹
aspect_ratio_estimation.py: 计算EAR 和 MAR的程序
dlib-19.7.0-cp36-cp36m-win_amd64.whl: dlib的安装文件 drivers_img_acquire.py: 获取驾驶人全景图像和人脸区域的程序
Eigen_Face_Recognizer.py: 特征脸识别器文件, 用特征脸识别不同驾驶人身份(效果并不好, 仅作为理论分析)
get_everybody_EARandMAR_standard.py: 得到每个驾驶人的EAR和MAR基准
haarcascade_eye.xml: 用于检测人眼睛位置的Haar级联分类器文件
haarcascade_frontalface_alt.xml: 用于检测人脸部位置的Harr级联分类器文件
head_posture_estimation.py: 头部姿态估计文件
main.py: 主函数, 用于处理拍摄好的视频图像
交流联系,主页或者点击这里 文章末尾----
下期再见