资讯详情

基于Python的驾驶员疲劳检测:EAR与MAR算法实战与避坑指南

📅 2026/10/9 23:39:03 | 华诺云谱 👁 阅读
基于Python的驾驶员疲劳检测:EAR与MAR算法实战与避坑指南
简介这份资源面向交通安全、计算机视觉方向的初学者与课程设计开发者提供一套基于Python的驾驶员疲劳检测完整实现包含源代码与图形化界面可用于毕业设计、课程作业或算法练手。压缩包共16个文件约84.55MB以6个py脚本和2个ipynb笔记本为核心辅以模型数据文件、界面工程文件、图标、演示视频与说明文档覆盖从数据采集、图像预处理、特征提取到疲劳评估与UI交互的完整链路。项目借助OpenCV与dlib提取眼部、嘴部及头部姿态特征结合机器学习或深度学习模型判断疲劳状态UI部分支持启动停止检测、实时结果展示与历史统计。目前已有106人学习下载读者可据此理解疲劳检测的工程结构、特征工程思路与界面集成方式并在此基础上替换数据集或模型进行二次开发。1. 驾驶员疲劳检测到底在检测什么从一次误报到完整方案跑长途的司机都有体会真正危险的不是闭眼那一下而是闭眼之前那几秒的恍惚。基于Python实现驾驶员疲劳检测源码UI界面这个方向核心就是用摄像头加算法在司机进入微睡眠之前给出预警。它检测的不是困不困这种主观感受而是几个可量化的生理信号眼睛闭合时长、眨眼频率、打哈欠次数、头部姿态偏移。适合谁做想入门计算机视觉的开发者、需要给车队做安全模块的工程师、以及拿它当毕设或练手项目的学生。整套方案的技术栈通常是OpenCV做图像采集、Dlib或MediaPipe做关键点定位、PyQt或Tkinter搭界面最后打包成一个能双击运行的桌面程序。下面把我实际搭过一遍的路径拆开讲包括参数怎么调、哪里容易翻车。2. 技术选型为什么是EAR加MAR这套组合拳2.1 眼睛纵横比EAR的原理与阈值边界EAREye Aspect Ratio是疲劳检测里最经典的指标思路很直接眼睛睁开时上下眼睑距离大闭合时距离趋近于零。用六个眼部关键点算纵横比公式是垂直方向两组距离之和除以水平距离的两倍。睁眼状态下这个值通常在0.25到0.35之间闭眼时会掉到0.15以下。关键点定位我一般用Dlib的68点模型第37到42点是左眼43到48点是右眼索引固定不用自己猜。import dlib import numpy as np from scipy.spatial import distance as dist def eye_aspect_ratio(eye_points): # eye_points: 6个(x, y)坐标顺序为左角、上左、上右、右角、下右、下左 # 垂直距离上眼睑两点到对应下眼睑两点的欧氏距离 vertical_a dist.euclidean(eye_points[1], eye_points[5]) vertical_b dist.euclidean(eye_points[2], eye_points[4]) # 水平距离左右眼角 horizontal dist.euclidean(eye_points[0], eye_points[3]) # 防止除零加极小值 ear (vertical_a vertical_b) / (2.0 * horizontal 1e-6) return ear # 初始化Dlib检测器和68点预测器 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) def get_eye_points(gray, rect): shape predictor(gray, rect) # 左眼37-42右眼43-48Dlib索引从1开始转成0基 left_eye [(shape.part(i).x, shape.part(i).y) for i in range(36, 42)] right_eye [(shape.part(i).x, shape.part(i).y) for i in range(42, 48)] return left_eye, right_eye这段代码里shape_predictor_68_face_landmarks.dat是Dlib的预训练模型文件需要单独下载放到工程目录。eye_aspect_ratio函数里加1e-6是血泪经验某些极端角度下水平距离会算出接近零的值不加这个保护程序直接抛异常。EAR阈值我一般设0.22但这个值跟摄像头分辨率、人脸在画面中的占比强相关不能照搬。2.2 嘴部纵横比MAR与打哈欠判定光看眼睛不够有人困了会频繁打哈欠这时候眼睛可能还睁着。MARMouth Aspect Ratio用嘴部关键点算纵横比Dlib的49到68点是嘴部区域。打哈欠时嘴巴张开MAR会从正常的0.1到0.2飙升到0.5以上。判定逻辑是MAR连续超过阈值且持续一定帧数计一次哈欠。def mouth_aspect_ratio(mouth_points): # 取上下唇内侧关键点Dlib中62-68为内唇 # 垂直距离用上唇中点到下唇中点 vertical dist.euclidean(mouth_points[13], mouth_points[19]) # 水平距离用左右嘴角 horizontal dist.euclidean(mouth_points[12], mouth_points[16]) mar vertical / (horizontal 1e-6) return mar # 在检测循环中 mar mouth_aspect_ratio(mouth_points) if mar 0.5: yawn_counter 1 else: if yawn_counter 15: # 连续15帧以上才算一次有效哈欠 total_yawns 1 yawn_counter 0这里yawn_counter 15的15帧不是拍脑袋定的。按30帧每秒算15帧约0.5秒能过滤掉说话、咀嚼这类短暂张嘴动作。如果摄像头帧率是60这个值要翻倍。MAR阈值0.5同样跟人脸尺度有关建议先用自己录的视频跑一遍看正常说话时MAR峰值到多少再往上留20%余量。2.3 头部姿态作为辅助信号眼睛和嘴巴之外头部持续低垂或频繁点头也是疲劳特征。用Dlib的68点可以估算头部姿态角但精度一般。更稳的做法是用solvePnP配合3D人脸模型算欧拉角或者直接用MediaPipe的Face Mesh它自带姿态估计。我一般把头部姿态作为辅助不单独触发报警而是和EAR、MAR做加权。比如EAR低于阈值且头部俯仰角大于15度才判定为疲劳这样能降低误报。3. 从零搭出可运行的检测主循环3.1 摄像头采集与关键点流水线主循环的结构是读帧、转灰度、检测人脸、提取关键点、算EAR和MAR、更新计数器、判断是否报警、绘制界面。这里有个性能坑Dlib的人脸检测在CPU上跑1080p画面每帧要几十毫秒帧率掉到十几。解决办法是先把画面缩到640宽再检测坐标按比例映射回原图。import cv2 cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) EYE_AR_THRESH 0.22 EYE_AR_CONSEC_FRAMES 20 # 连续20帧闭眼触发报警 frame_counter 0 alarm_on False while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 直方图均衡化逆光环境下关键点更稳 gray cv2.equalizeHist(gray) rects detector(gray, 0) for rect in rects: left_eye, right_eye get_eye_points(gray, rect) ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 if ear EYE_AR_THRESH: frame_counter 1 if frame_counter EYE_AR_CONSEC_FRAMES: if not alarm_on: alarm_on True # 触发报警播放声音或弹窗 else: frame_counter 0 alarm_on False cv2.imshow(Fatigue Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()EYE_AR_CONSEC_FRAMES 20对应约0.67秒的持续闭眼。正常眨眼闭眼时间约0.1到0.4秒20帧能有效区分眨眼和微睡眠。如果设成10帧正常眨眼会被误判设成30帧报警又太迟。这个值我调过好几轮20是误报和漏报比较平衡的点。equalizeHist在夜间或逆光场景下很有用但光线均匀时反而可能引入噪声可以做成开关。3.2 用PyQt5搭一个能看的UI界面命令行窗口跑通了下一步是套界面。PyQt5是常见选择把视频显示在QLabel上旁边放几个状态灯和参数输入框。核心是把OpenCV的BGR帧转成QImage再转QPixmap。from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import QTimer import cv2 import sys class FatigueUI(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(驾驶员疲劳检测) self.central QWidget() self.layout QVBoxLayout() self.video_label QLabel() self.status_label QLabel(状态正常) self.layout.addWidget(self.video_label) self.layout.addWidget(self.status_label) self.central.setLayout(self.layout) self.setCentralWidget(self.central) self.cap cv2.VideoCapture(0) self.timer QTimer() self.timer.timeout.connect(self.update_frame) self.timer.start(30) # 约33帧每秒 def update_frame(self): ret, frame self.cap.read() if not ret: return # 这里插入检测逻辑更新status_label rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qt_img QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qt_img)) def closeEvent(self, event): self.cap.release() app QApplication(sys.argv) window FatigueUI() window.show() sys.exit(app.exec_())timer.start(30)里的30是毫秒对应约33帧每秒。如果检测逻辑耗时超过30毫秒界面会卡顿这时候要么降分辨率要么把检测放到独立线程。我一般用QThread把检测和界面刷新分开界面只负责显示检测线程算完把结果通过信号发过来。closeEvent里释放摄像头是必须的不然程序关了摄像头还占着下次运行报设备忙。3.3 报警策略声音、弹窗还是震动报警方式看使用场景。桌面程序一般用pygame播放提示音或者直接调系统蜂鸣。更柔和的做法是界面状态灯变红加文字提示。如果做车载嵌入式可以接GPIO驱动震动马达。我建议报警分两级EAR低于阈值但未超时状态灯变黄超过连续帧数状态灯变红并播放声音。这样司机有个缓冲不会突然被吓一跳。import pygame pygame.mixer.init() alarm_sound pygame.mixer.Sound(alarm.wav) def trigger_alarm(): if not pygame.mixer.get_busy(): alarm_sound.play()pygame.mixer.get_busy()防止声音重叠播放不然连续触发时声音会叠成一片噪音。报警音文件自己录一段或找个短促的提示音就行别用太刺耳的长期用会让人烦躁反而分心。4. 避坑指南调参和部署时最容易翻车的五个点4.1 光照一变EAR就飘现象白天调好的阈值晚上或者进隧道后疯狂误报。原因Dlib关键点定位依赖灰度纹理光照不足时眼角点会偏移EAR算出来忽高忽低。解决加equalizeHist做直方图均衡或者用红外摄像头。更稳的办法是动态阈值——用前5秒的EAR均值乘以0.7作为当前阈值适应不同光照。4.2 戴眼镜反光导致关键点丢失现象戴眼镜的测试者一转头关键点跳到镜框上EAR突变。原因镜片反光干扰了特征点回归。解决换MediaPipe Face Mesh它对眼镜的鲁棒性比Dlib好或者在预处理里加去反光但效果有限。实际项目里我一般建议测试者摘掉反光严重的眼镜或者用带红外补光的摄像头。4.3 帧率不稳导致计数器失效现象明明闭眼时间够长但报警没触发。原因摄像头帧率波动按帧计数不准。解决改用时间戳计算持续时长而不是数帧。记录闭眼开始的time.time()当前时间减去开始时间超过0.6秒就触发这样跟帧率解耦。import time eye_closed_start None if ear EYE_AR_THRESH: if eye_closed_start is None: eye_closed_start time.time() elif time.time() - eye_closed_start 0.6: trigger_alarm() else: eye_closed_start None4.4 打包成exe后模型文件找不到现象源码跑得好好的PyInstaller打包后报shape_predictor_68_face_landmarks.dat找不到。原因打包时模型文件没被包含或者路径用了相对路径。解决用--add-data把dat文件打进去代码里用sys._MEIPASS拼路径。import sys import os def resource_path(relative): if hasattr(sys, _MEIPASS): return os.path.join(sys._MEIPASS, relative) return os.path.join(os.path.abspath(.), relative) predictor dlib.shape_predictor(resource_path(shape_predictor_68_face_landmarks.dat))4.5 多人脸场景只处理最大人脸现象画面里两个人程序在两张脸之间跳EAR乱变。原因检测到多个人脸时没做筛选。解决按人脸框面积排序只取最大的那个或者加一个驾驶员位置的ROI区域只检测画面左侧或中间的人脸。rects sorted(rects, keylambda r: r.width() * r.height(), reverseTrue) if len(rects) 0: rect rects[0] # 只处理最大人脸5. 进阶把误报率压到可接受范围的三个技巧第一个技巧是融合多信号做决策。单独看EAR低头捡东西也会触发单独看MAR说话也会触发。把EAR、MAR、头部姿态三个信号做加权投票比如EAR权重0.5、MAR权重0.3、头部姿态0.2总分超过0.7才报警。这样误报能降一个数量级。权重怎么定拿一段自己录的模拟疲劳视频和一段正常驾驶视频跑网格搜索找误报和漏报的平衡点。第二个技巧是加时间窗口平滑。不要用单帧的EAR做判断用最近30帧的滑动平均。这样偶尔一帧关键点抖动不会触发报警。滑动窗口大小按帧率定30帧每秒就用30对应1秒。窗口太小平滑不够太大报警延迟。from collections import deque ear_window deque(maxlen30) ear_window.append(ear) smooth_ear sum(ear_window) / len(ear_window) if smooth_ear EYE_AR_THRESH: # 后续逻辑第三个技巧是分场景标定。不同车型、不同摄像头安装角度EAR基线不一样。做一个标定模式程序启动后前10秒让司机正常睁眼记录EAR均值作为基线阈值设为基线的0.65倍。这样换车换摄像头不用改代码跑一遍标定就行。验证方法上我一般用两个指标误报率正常驾驶10分钟内报警次数和漏报率模拟疲劳时未报警次数。正常驾驶视频可以自己正常开车录一段模拟疲劳视频可以刻意眯眼、打哈欠。目标是把误报控制在10分钟1次以内漏报尽量为零。如果误报还是高优先调高连续帧数阈值牺牲一点响应速度换稳定性。最后说个我自己的习惯每次调完参数一定用同一段测试视频跑三遍看报警时间点是否一致。如果三遍结果差很多说明参数在临界点附近得往安全方向挪一挪。这套方案不算复杂但细节决定它能不能真正用起来。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑