news 2026/10/9 15:25:36

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于EAR/MAR/PERCLOS的驾驶员疲劳检测:Python+OpenCV+Dlib实战与避坑

简介:本资源面向交通安全与计算机视觉方向的开发者、学生及科研人员,提供一套基于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 分类器判断眼睛睁闭,但问题很明显:需要大量标注数据,且不同人种、光照、眼镜反光下泛化差。EAR(Eye 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:打哈欠和长时间闭眼怎么量化

光看眼睛不够,疲劳的另一个强信号是打哈欠。MAR(Mouth 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, key=lambda 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_score

EYE_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_seconds=30, fps=30): self.window_size = window_seconds * fps self.buffer = deque(maxlen=self.window_size) def update(self, ear, threshold=0.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_seconds=30, fps=30)

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(side=tk.LEFT) self.info_frame = tk.Frame(root) self.info_frame.pack(side=tk.RIGHT, fill=tk.Y) self.ear_label = tk.Label(self.info_frame, text="EAR: --") self.ear_label.pack() self.mar_label = tk.Label(self.info_frame, text="MAR: --") self.mar_label.pack() self.status_label = tk.Label(self.info_frame, text="状态: 正常", font=("Arial", 16), fg="green") 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(image=img) self.video_label.imgtk = imgtk self.video_label.configure(image=imgtk) self.ear_label.config(text=f"EAR: {ear:.3f}") self.mar_label.config(text=f"MAR: {mar:.3f}") color = "red" if status == "疲劳" else "green" self.status_label.config(text=f"状态: {status}", fg=color)

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_frames=100): 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 做移动端,中间踩的坑基本都在上面了。最深的教训是:别在算法精度上死磕,先把摄像头位置和光照搞定。摄像头装在方向盘正上方、稍微偏驾驶员一侧,比换任何模型都管用。另外阈值一定要做个人校准,固定值只能用来演示,真上车必须自适应。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 15:24:18

智算中心建设实操指南:从机柜布线到NCCL调优

简介&#xff1a;本资源是一份面向政企信息化建设者、数据中心规划师及AI基础设施从业者的智算中心项目落地实施方案&#xff0c;聚焦西部地区&#xff08;以贵州为典型&#xff09;如何依托‘东数西算’政策红利构建弹性可扩展、算力多元化、绿色高效的区域级算力枢纽。PPT共4…

作者头像 李华
网站建设 2026/10/9 15:21:10

PHP留言板源码+MySQL数据库部署与安全改造全流程解析

简介&#xff1a;PHP留言板源码包内含MySQL数据库文件&#xff0c;是一套面向PHP与MySQL初学者的完整Web入门项目&#xff0c;适合课程设计、毕业设计或自主练手&#xff0c;可帮助快速搭建带用户注册登录、留言发布与展示的互动页面。资源共131个文件&#xff0c;压缩包仅746K…

作者头像 李华
网站建设 2026/10/9 15:17:20

.NET混淆器实战:dotNET_Reactor汉化版安装配置与避坑指南

简介&#xff1a;dotNET_Reactor 汉化版是一款面向 .NET 开发者的实用混淆与代码保护工具&#xff0c;主要帮助解决程序被反编译、调试、篡改等风险&#xff0c;适合发布商业软件、插件或对安全性有要求的 .NET 2.0 至 .NET 5 开发者。压缩包共 6 个文件、约 2.58MB&#xff0c…

作者头像 李华
网站建设 2026/10/9 15:15:03

第 37 章 · 综合项目二:3D 点云与刚体变换

第二个实战项目&#xff1a;处理 3D 点云&#xff0c;做旋转和平移。这是计算机图形学、机器人、SLAM 的基础。本项目综合运用&#xff1a;vector、几何模块、刚体变换。37.1 什么是点云 点云&#xff08;point cloud&#xff09; 是一堆三维点的集合。激光雷达扫描、3D 扫描仪…

作者头像 李华
网站建设 2026/10/9 15:14:22

HarmonyOS 7 AccessToken:权限触发链校验与审核证据归档【鸿蒙心迹】

有一次做提交前自查&#xff0c;权限声明看起来没有问题&#xff1a;module.json5 里写了相机权限&#xff0c;页面也有隐私说明&#xff0c;测试机器上拍照流程通了。可一旦换成从未授权的新用户&#xff0c;问题就冒出来&#xff1a;他打开首页时为什么已经出现权限对话框&am…

作者头像 李华
网站建设 2026/10/9 15:05:23

北理工数据库上机实验包:五阶能力闭环实战指南

简介&#xff1a;本资源是北京理工大学计算机学院“数据库原理与设计”课程配套上机实验材料&#xff0c;面向高校计算机专业本科生及数据库初学者&#xff0c;聚焦关系数据库理论落地与SQL工程实践能力培养。压缩包共12个文件&#xff0c;含4个核心SQL脚本&#xff08;覆盖建库…

作者头像 李华