资讯动态

基于EAR/MAR/PERCLOS的驾驶员疲劳检测:Python+OpenCV+Dlib实战与避坑

发布时间:2026/10/9 15:25:58 来源:尧图企业网站定制
简介本资源面向交通安全与计算机视觉方向的开发者、学生及科研人员提供一套基于Python的驾驶员疲劳检测完整实现方案用于识别驾驶者疲劳状态以预防疲劳驾驶事故。项目整合视频流处理、面部特征提取、疲劳评估与图形化交互界面适合作为课程设计、毕业设计或算法练手项目。压缩包共16个文件约84.55MB包含6个py源码、2个ipynb实验笔记、1个dat人脸关键点模型、1个mp4测试视频、1个exe安装包及png、jpg、ico等界面素材覆盖数据采集、预处理、特征提取、疲劳评估与UI展示等模块。已有106人学习下载。读者可获得可运行的检测源码、OpenCV与机器学习算法实现思路、基于68点人脸关键点的眼部与嘴部状态分析逻辑以及可直接复用的UI界面工程与测试视频便于快速复现并二次开发。1. 从一张摄像头截图说起驾驶员疲劳检测到底在做什么很多人第一次接触驾驶员疲劳检测脑子里想的是「AI 判断我困不困」但真正落地时你会发现它其实是一套从人脸关键点到疲劳判据的流水线。摄像头每帧给你一张图算法先找到人脸再定位眼睛和嘴巴的位置然后计算眼睛闭合程度、眨眼频率、打哈欠次数最后综合成一个疲劳分数。这套逻辑不依赖什么大模型用 Python 加 OpenCV 和 Dlib 就能跑起来源码加 UI 界面的组合包之所以常见是因为它把「算法验证」和「演示交互」两件事打包了适合做课程设计、原型验证或者车载终端的算法预研。这篇文章面向两类人一类是刚拿到类似源码包、想搞清楚每一行在干什么的新手另一类是想把这套东西从 Demo 推到实际场景、需要知道参数边界和翻车点的熟手。我会按「原理选型 → 环境搭建 → 核心算法实现 → UI 集成 → 避坑 → 进阶调优」的顺序拆开讲代码可以直接抄参数会告诉你为什么这么设。2. 疲劳判据怎么选EAR、MAR 与 PERCLOS 的工程取舍2.1 为什么是 EAR 而不是直接分类睁眼闭眼早期做法是训练一个 CNN 分类器判断眼睛睁闭但问题很明显需要大量标注数据且不同人种、光照、眼镜反光下泛化差。EAREye Aspect Ratio走的是几何路线用眼睛周围六个关键点的纵横比来描述睁闭状态公式是EAR (|p2-p6| |p3-p5|) / (2 * |p1-p4|)睁眼时这个值在 0.25~0.35 之间闭眼时迅速掉到 0.1 以下。它的好处是不需要训练换个人只要重新标定阈值就能用计算量几乎可以忽略。代价是对关键点检测精度敏感侧脸或遮挡时关键点漂移会导致误判。我一般会先用 Dlib 的 68 点模型跑通因为它的 6 个眼部点索引是固定的左眼 36-41右眼 42-47。这个索引顺序在多个开源实现里通用抄代码时不容易错位。2.2 MAR 与 PERCLOS打哈欠和长时间闭眼怎么量化光看眼睛不够疲劳的另一个强信号是打哈欠。MARMouth Aspect Ratio用嘴巴上下唇的关键点距离除以嘴巴宽度正常说话时在 0.2~0.4打哈欠时会飙到 0.6 以上并持续 1 秒以上。Dlib 的 68 点里嘴巴是 48-67取 62 和 66 作为上下唇内点60 和 64 作为左右嘴角就能算出 MAR。PERCLOS 则是另一个维度的指标单位时间内眼睛闭合时间占比。它不看你某一帧闭没闭眼而是统计 30 秒或 60 秒窗口内 EAR 低于阈值的帧数比例。这个指标对「微睡眠」特别敏感——那种眼睛半闭不闭、你自己都没意识到的状态单帧 EAR 可能只是略低但 PERCLOS 会明显上升。工程上我通常三个指标一起用EAR 触发即时闭眼计数MAR 触发哈欠计数PERCLOS 做长周期疲劳累积。三者加权求和权重根据场景调——高速场景对 PERCLOS 更敏感城市低速对哈欠计数更敏感。2.3 关键点检测器的选型对比方案速度CPU 单帧精度依赖适用场景Dlib 68 点15-25ms中dlib 模型文件原型验证、课程设计MediaPipe Face Mesh5-10ms高mediapipe移动端、实时性要求高OpenCV DNN 自定义模型10-20ms取决于训练opencv-contrib需要定制关键点Dlib 的优势是索引固定、资料多缺点是模型文件 99MB 左右且对侧脸鲁棒性一般。MediaPipe 的 468 点更密但索引体系不同网上抄来的 EAR 代码不能直接套。如果你拿到的源码包用的是 Dlib就先把 Dlib 这条路走通别中途换。3. 把环境跑起来Dlib 编译与摄像头管线的三个关键配置3.1 安装 Dlib 不翻车的两种方式Dlib 在 Windows 上直接 pip install 经常卡在编译因为需要 CMake 和 Visual Studio Build Tools。我一般推荐先用 conda 装预编译版# 方式一conda 预编译最省事 conda install -c conda-forge dlib # 方式二pip 安装需要先装 CMake 和 VS Build Tools pip install cmake pip install dlib如果 pip 编译报错「Cannot find cmake」先确认 cmake 在 PATH 里报错「Visual Studio not found」就装 VS Build Tools 并勾选 C 桌面开发。Linux 下相对简单sudo apt install cmake build-essential之后 pip 基本能过。3.2 摄像头读取与帧率控制很多源码包直接cv2.VideoCapture(0)然后 while 循环结果 CPU 跑满、画面延迟越来越大。问题在于没有控制读取节奏也没有释放缓冲。我一般会加两个设置import cv2 cap cv2.VideoCapture(0) # 设置缓冲区为 1避免读到旧帧 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 设置分辨率降低计算量 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ret, frame cap.read() if not ret: break # 水平翻转符合镜子习惯 frame cv2.flip(frame, 1) # 后续处理... if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()CAP_PROP_BUFFERSIZE设成 1 是关键默认缓冲可能积压好几帧导致你看到的画面和实际动作差半秒。分辨率降到 640x480 对 EAR 计算精度影响很小但速度能提升一倍以上。cv2.flip看个人习惯不做也不影响算法。3.3 关键点检测的初始化与 ROI 裁剪Dlib 的检测器初始化一次就行不要每帧都创建import dlib detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) def get_landmarks(gray, detector, predictor): # 1 表示上采样一次提高小脸检测率 faces detector(gray, 1) if len(faces) 0: return None # 只取最大人脸避免多人干扰 face max(faces, keylambda r: r.width() * r.height()) shape predictor(gray, face) return [(shape.part(i).x, shape.part(i).y) for i in range(68)]detector(gray, 1)里的 1 是上采样次数能提高远距离小脸检测率但会拖慢速度。如果摄像头离得近设 0 就够。max(faces, key...)取最大人脸是为了在多人场景下锁定驾驶员不然关键点会跳到副驾驶脸上。4. 核心算法实现EAR/MAR 计算与疲劳状态机4.1 EAR 与 MAR 的 Python 实现import numpy as np from scipy.spatial import distance as dist def eye_aspect_ratio(eye_points): # eye_points: 6 个点的列表 [(x,y), ...] # 垂直距离上眼睑下沿到上沿 A dist.euclidean(eye_points[1], eye_points[5]) B dist.euclidean(eye_points[2], eye_points[4]) # 水平距离左右眼角 C dist.euclidean(eye_points[0], eye_points[3]) ear (A B) / (2.0 * C) return ear def mouth_aspect_ratio(mouth_points): # 取内唇上下点和左右嘴角 # 68 点中60 左嘴角64 右嘴角62 上唇内66 下唇内 A dist.euclidean(mouth_points[2], mouth_points[6]) # 62 vs 66 B dist.euclidean(mouth_points[0], mouth_points[4]) # 60 vs 64 mar A / B return mar注意eye_points的顺序必须和 Dlib 索引一致左眼 36-41 依次是左眼角、上眼睑两点、右眼角、下眼睑两点。如果顺序错位EAR 会算出完全错误的值。我见过有人把 36-41 直接切片后不排序就传进去结果 EAR 一直在 0.5 以上闭眼都测不出来。4.2 疲劳状态机从单帧判断到连续计数单帧 EAR 低于阈值不能直接报警眨眼也会短暂低于阈值。需要一个计数器加时间窗口EYE_AR_THRESH 0.25 # EAR 阈值 EYE_AR_CONSEC_FRAMES 3 # 连续帧数阈值 MOUTH_AR_THRESH 0.6 MOUTH_CONSEC_FRAMES 15 # 哈欠持续帧数 eye_counter 0 mouth_counter 0 total_blinks 0 total_yawns 0 fatigue_score 0 def update_state(ear, mar): global eye_counter, mouth_counter, total_blinks, total_yawns, fatigue_score # 闭眼计数 if ear EYE_AR_THRESH: eye_counter 1 else: if eye_counter EYE_AR_CONSEC_FRAMES: total_blinks 1 fatigue_score 1 eye_counter 0 # 哈欠计数 if mar MOUTH_AR_THRESH: mouth_counter 1 else: if mouth_counter MOUTH_CONSEC_FRAMES: total_yawns 1 fatigue_score 3 mouth_counter 0 # 疲劳分数衰减避免无限累积 fatigue_score max(0, fatigue_score - 0.01) return fatigue_scoreEYE_AR_CONSEC_FRAMES 3是经验值30fps 下约 0.1 秒能过滤掉正常眨眼。如果你摄像头只有 15fps这个值要降到 2否则正常眨眼会被算成闭眼。fatigue_score的衰减系数 0.01 是每帧减一点让分数不会只增不减具体值根据你希望的「恢复速度」调。4.3 PERCLOS 的滑动窗口实现from collections import deque class PerclosCalculator: def __init__(self, window_seconds30, fps30): self.window_size window_seconds * fps self.buffer deque(maxlenself.window_size) def update(self, ear, threshold0.25): # 1 表示闭眼0 表示睁眼 self.buffer.append(1 if ear threshold else 0) if len(self.buffer) self.window_size: return 0.0 return sum(self.buffer) / len(self.buffer) perclos_calc PerclosCalculator(window_seconds30, fps30)PERCLOS 超过 0.15 通常认为进入疲劳状态超过 0.3 是严重疲劳。这个阈值不是绝对的跟个人眼睛大小有关建议先用自己录一段正常驾驶视频跑一遍看基线在哪。5. UI 界面集成Tkinter 与 OpenCV 画面嵌入的实操5.1 为什么选 Tkinter 而不是 PyQt源码包里常见 Tkinter因为它是 Python 自带的不需要额外装 Qt 那套几百 MB 的依赖。缺点是界面丑、刷新率有限但做疲劳检测的演示足够了。核心思路是把 OpenCV 的帧转成 PIL 图像再塞进 Tkinter 的 Label 里。import tkinter as tk from PIL import Image, ImageTk import cv2 class FatigueUI: def __init__(self, root): self.root root self.root.title(疲劳检测演示) self.video_label tk.Label(root) self.video_label.pack(sidetk.LEFT) self.info_frame tk.Frame(root) self.info_frame.pack(sidetk.RIGHT, filltk.Y) self.ear_label tk.Label(self.info_frame, textEAR: --) self.ear_label.pack() self.mar_label tk.Label(self.info_frame, textMAR: --) self.mar_label.pack() self.status_label tk.Label(self.info_frame, text状态: 正常, font(Arial, 16), fggreen) self.status_label.pack() def update_frame(self, frame, ear, mar, status): # BGR 转 RGB rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img Image.fromarray(rgb) imgtk ImageTk.PhotoImage(imageimg) self.video_label.imgtk imgtk self.video_label.configure(imageimgtk) self.ear_label.config(textfEAR: {ear:.3f}) self.mar_label.config(textfMAR: {mar:.3f}) color red if status 疲劳 else green self.status_label.config(textf状态: {status}, fgcolor)self.video_label.imgtk imgtk这行必须保留否则图像对象会被垃圾回收界面显示空白。这是 Tkinter 嵌入 OpenCV 最经典的坑。5.2 主循环与 UI 刷新节奏Tkinter 的after方法比while循环更适合做刷新因为它不阻塞主线程def video_loop(): ret, frame cap.read() if ret: frame cv2.flip(frame, 1) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) landmarks get_landmarks(gray, detector, predictor) if landmarks: left_eye [landmarks[i] for i in range(36, 42)] right_eye [landmarks[i] for i in range(42, 48)] mouth [landmarks[i] for i in range(48, 68)] ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 mar mouth_aspect_ratio(mouth) score update_state(ear, mar) status 疲劳 if score 10 else 正常 ui.update_frame(frame, ear, mar, status) else: ui.update_frame(frame, 0, 0, 未检测到人脸) root.after(30, video_loop) # 约 33fps root tk.Tk() ui FatigueUI(root) root.after(0, video_loop) root.mainloop()root.after(30, video_loop)里的 30 是毫秒对应约 33fps。如果你摄像头是 30fps设 33 更匹配。设太小会 CPU 飙高设太大画面卡顿。6. 避坑与排查五个让检测翻车的真实原因6.1 现象EAR 一直偏高闭眼测不出来原因关键点索引顺序错了或者用了 MediaPipe 的索引去套 Dlib 的点。Dlib 左眼 36-41 的顺序是固定的但有人从网上抄了另一套顺序导致垂直距离算成了水平距离。解决打印出 36-41 的坐标画在图上确认。左眼 36 是左眼角39 是右眼角37/38 是上眼睑40/41 是下眼睑。如果 37 和 41 的 y 坐标差很小说明顺序反了。6.2 现象戴眼镜的人检测率骤降原因镜片反光导致关键点漂移尤其是红外摄像头下反光更严重。另外镜框可能被误检为眼睛轮廓。解决换用 MediaPipe 的 Face Mesh它对眼镜的鲁棒性更好。如果必须用 Dlib把detector(gray, 1)的上采样改成 0减少反光区域的误检同时把 EAR 阈值从 0.25 降到 0.22补偿关键点偏移。6.3 现象疲劳分数只增不减休息后也不恢复原因衰减系数设得太小或者根本没有衰减逻辑。有些源码包只做累加跑十分钟分数就爆了。解决加衰减每帧减 0.01~0.05具体值看你希望的恢复时间。另外可以设一个上限比如 100超过就报警并重置。6.4 现象UI 画面卡顿但算法本身不慢原因Tkinter 的PhotoImage每帧都创建新对象内存回收跟不上。或者after的间隔设得太小UI 线程被刷屏任务占满。解决复用PhotoImage对象或者用PIL.ImageTk.PhotoImage的paste方法更新。间隔不要低于 25ms否则 Tkinter 渲染不过来。6.5 现象夜间或逆光下完全检测不到人脸原因Dlib 的 HOG 检测器对光照敏感暗光下梯度特征消失。解决加一个简单的直方图均衡化预处理gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) # 增强对比度如果还不行就得换红外摄像头或者用基于深度学习的检测器。普通 RGB 摄像头在完全无光环境下无解这是物理限制。7. 进阶调优自适应阈值与多指标融合的实用技巧7.1 用前 100 帧做个人基线校准固定阈值 0.25 对眼睛小的人偏松对眼睛大的人偏紧。我一般会在程序启动后先采集 100 帧睁眼状态算 EAR 均值然后阈值设为均值的 70%class AdaptiveThreshold: def __init__(self, calibrate_frames100): self.calibrate_frames calibrate_frames self.ear_samples [] self.threshold 0.25 # 默认值 def update(self, ear): if len(self.ear_samples) self.calibrate_frames: self.ear_samples.append(ear) if len(self.ear_samples) self.calibrate_frames: mean_ear np.mean(self.ear_samples) self.threshold mean_ear * 0.7 print(f校准完成EAR 均值 {mean_ear:.3f}阈值 {self.threshold:.3f}) return self.threshold校准期间 UI 上显示「校准中」避免用户以为程序坏了。100 帧在 30fps 下约 3.3 秒可以接受。7.2 多指标融合的加权策略单独看 EAR 容易把「眯眼」误判为疲劳单独看 MAR 会把「说话」误判为哈欠。我一般用下面的加权公式指标权重触发条件说明闭眼计数1EAR 阈值且持续 3 帧正常眨眼不计哈欠计数3MAR 0.6 且持续 15 帧说话不会持续这么久PERCLOS1030 秒窗口 0.15长周期累积头部姿态2低头超过 20 度持续 2 秒需要额外算姿态头部姿态可以用cv2.solvePnP从 68 点里挑几个稳定点算但会增加计算量。如果只是做课程设计前三个指标够了。7.3 报警策略分级而不是一刀切不要一疲劳就蜂鸣器狂响分级更实用def get_alert_level(score): if score 5: return 正常, green elif score 15: return 轻度疲劳, orange elif score 30: return 中度疲劳, red else: return 严重疲劳, darkred轻度疲劳只改 UI 颜色中度加声音提示严重才触发蜂鸣器。这样不会因为一次误判就吓到驾驶员。7.4 录制回放做回归测试调参最怕改了一个值另一个场景崩了。我习惯用cv2.VideoWriter把测试视频存下来每次改完参数跑一遍回放对比疲劳触发时间点# 录制 fourcc cv2.VideoWriter_fourcc(*XVID) out cv2.VideoWriter(test_drive.avi, fourcc, 30.0, (640, 480)) # 回放时把 cap cv2.VideoCapture(0) 改成 cap cv2.VideoCapture(test_drive.avi)回放时把cv2.imshow关掉只跑算法逻辑速度能快好几倍。我一般会录三段正常驾驶、打哈欠、闭眼微睡眠每段 30 秒改完参数就跑这三段看误报和漏报。这套东西我从最早用 Dlib 跑通到后来换成 MediaPipe 做移动端中间踩的坑基本都在上面了。最深的教训是别在算法精度上死磕先把摄像头位置和光照搞定。摄像头装在方向盘正上方、稍微偏驾驶员一侧比换任何模型都管用。另外阈值一定要做个人校准固定值只能用来演示真上车必须自适应。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑