☰
Python+Dlib实现疲劳检测:人脸关键点与EAR算法详解
2026/10/1 11:49:13 网站建设 项目流程

简介:基于Python与Dlib的驾驶员疲劳检测系统,围绕打哈欠、眨眼、瞌睡点头三个典型疲劳特征,通过人脸朝向、瞳孔开合度、眨眼频率等实时计算驾驶员注意力集中程度,并在疲劳迹象出现时及时提示。系统配有可视化界面,可用于毕业设计、课程设计或实际场景演示,适合计算机、人工智能、自动化、电子信息等专业学生及开发者参考学习。压缩包共17个文件,体积约85.92MB,包含核心Python源码(眼睛检测、嘴巴检测、点头检测模块)、Dlib人脸68点特征模型、Jupyter演示脚本、可视化界面文件、安装配置工具、运行效果图及测试视频等,目录结构清晰,便于按模块查看与调试;附带的人脸模型与安装工具可降低环境配置门槛。已有232人学习下载,方案在疲劳检测类题目中具有一定参考价值。除可直接运行的完整代码外,还配有README说明、界面设计源文件与演示视频,可帮助读者快速掌握从算法到界面的实现思路;稍加修改即可扩展为独立作品,适合作为毕业设计基础进行二次开发。

1. 用Python和Dlib做疲劳检测:为什么毕设都选这条路线

你可能已经翻过不少开源项目,发现用深度学习做疲劳检测的demo很多,但真正敢拿到答辩现场跑的很少。原因很简单:深度学习方案要训练数据、要标注、要GPU,而大部分毕设现场只有一台笔记本和一个USB摄像头。Dlib这个老牌C++库配合Python走的是另一条路——用68个预训练人脸关键点,把眨眼、打哈欠、点头这些动作换算成几个几何比例值,不训练模型,CPU实时跑。这就是“基于Python+Dlib的驾驶员疲劳检测”在毕业设计里经久不衰的原因:工作量可控、原理能讲透、界面演示直观,还自带一套可复现的评价指标。

这个方案本质上是把“疲劳”量化成眼睛纵横比、嘴部纵横比和头部角度三个数字,再结合连续帧判定输出状态。它能解决的核心问题是:在不依赖昂贵设备的前提下,用普通摄像头完成眨眼计数、哈欠识别、瞌睡点头检测,并落到可视化界面上。适合正在做毕设、想在CV方向快速出成果的学生,也适合想拿现成代码做二次开发的入门工程师。

2. Dlib的68个关键点:把“眨眼”和“哈欠”换算成两个数字

2.1 为什么选Dlib而不是OpenCV人脸检测或深度学习模型

做疲劳检测第一件要决定的事,是人脸检测和关键点提取用什么。OpenCV自带的Haar级联也能框出人脸,但它只给你一个矩形框,拿不到眼睛和嘴巴的坐标,后续的疲劳判断无从谈起。深度学习模型倒是有现成的关键点输出,比如OpenCV的DNNS模块和MediaPipe,但打包体积大,推理速度在纯CPU环境下也吃紧。

Dlib的位置刚好卡在中间。dlib.get_frontal_face_detector()用的是HOG特征加线性SVM分类器,对正脸检测速度极快;关键点提取用的是ERT(Ensemble of Regression Trees)回归模型,输入一张人脸框,直接输出68个关键点坐标。这套组合在笔记本CPU上处理640x480的画面能做到20~30帧,对实时检测来说够了。

另一个现实理由是模型文件好拿。Dlib官方训练好的shape_predictor_68_face_landmarks.dat约百兆,把68点坐标对应到人脸各部位是固定的索引号,不需要自己标数据。虽然Dlib的关键点提取器是个黑匣子——你输入图片它返回坐标,中间做了什么不完全透明——但对于毕设级别的应用,这种“可用即可”的接口反而省事。

2.2 EAR眼睛纵横比:一个比例值识别眨眼和闭眼

68个关键点中,左右眼睛各占6个点。左眼是索引36到41,右眼是索引42到47。如果你把每个点按顺序连起来,会得到一个近似眼裂形状的多边形。

2016年一篇用面部关键点做实时眨眼检测的论文提出了EAR(Eye Aspect Ratio,眼睛纵横比),公式是用眼睛的纵向距离除以横向距离:

from math import hypot def eye_aspect_ratio(eye_points): # eye_points 是6个关键点的(x,y)坐标列表 # 纵向距离取两组垂直对角点的平均值 vertical_1 = hypot(eye_points[1][0] - eye_points[5][0], eye_points[1][1] - eye_points[5][1]) vertical_2 = hypot(eye_points[2][0] - eye_points[4][0], eye_points[2][1] - eye_points[4][1]) # 横向距离取水平对角点 horizontal = hypot(eye_points[0][0] - eye_points[3][0], eye_points[0][1] - eye_points[3][1]) return (vertical_1 + vertical_2) / (2.0 * horizontal)

这段代码的关键在分母。横向距离是眼睛睁开时的固定基线,纵向距离随眼皮闭合快速变小,所以EAR的值在清醒时稳定在0.25~0.35,闭眼时掉到0.1以下。用比例而不是绝对像素距离,意味着人脸离摄像头远近不影响判断——同一个人的EAR在1米和2米距离下数值基本不变,这就是它适合做阈值的根本原因。

2.3 用嘴部纵横比MAR和下巴夹角识别哈欠与点头

打哈欠的检测思路和眨眼一致,换到嘴部区域。嘴部外轮廓一般在索引48到59之间,取其中6个点(左嘴角、右上唇、右下唇、右嘴角、左下唇、左上唇)算一个嘴部纵横比MAR。哈欠时嘴巴张大,纵向距离显著拉大,MAR超过正常说话时的数值。阈值通常取0.5以上,但也要根据个人习惯微调。

点头检测常见做法是取鼻尖点(索引30)和下巴点(索引8)连一条直线,计算这条线与垂直方向的夹角。头部竖直时夹角接近0度,低头时夹角变大。每帧计算一次角度,连续若干帧角度变化超过10度,判为一次点头动作。

import math def head_pitch(landmarks): # 鼻尖点30,下巴点8 nose = landmarks[30] chin = landmarks[8] dx = nose[0] - chin[0] dy = nose[1] - chin[1] angle = math.degrees(math.atan2(dx, dy)) return abs(angle)

这里有个取舍:更专业的做法是用cv2.solvePnP配合3D人脸模型解算头部欧拉角,但需要相机内参标定,对毕设来说负担偏重。用两点连线的角度变化近似点头幅度,虽然粗糙,但配合连续帧累计已经足够抓出明显的瞌睡点头。想要效果更稳,可以对角度做滑动平均,再和阈值比较。

3. 跑通检测主循环:摄像头取帧、关键点计算与连续闭眼判定

3.1 环境准备:Python安装、Dlib编译依赖与模型文件

先把环境弄清楚。Python建议用3.8到3.10的版本,Anaconda安装最顺手。如果你还没配好环境,先去搜一篇python安装教程把基础环境落地,装好后用conda create -n fatigue python=3.8建一个独立虚拟环境,避免把系统Python搞乱。用vscode的话记得调一下vscode python环境配置,把解释器切到conda的fatigue环境,否则后面import dlib会报ModuleNotFoundError。

装Dlib是第一个可能翻车的点。新版Dlib在Windows和Linux上都有预编译wheel,直接pip install dlib一般能过。如果pip开始源码编译并卡在“Building wheel for dlib”,说明你的机器缺C++编译链,需要先装Visual Studio Build Tools(Windows)或build-essential(Linux),再重试。用Anaconda的用户也可以走conda install -c conda-forge dlib,这条路径会避开不少编译问题。

模型文件要去Dlib的官方GitHub仓库Releases里拿shape_predictor_68_face_landmarks.dat,约百兆,放到项目的models目录下。加载时写成绝对路径或用os.path.join拼接,避免相对路径在IDE和命令行下行为不一致。

pip install dlib opencv-python mkdir models # 将下载的 shape_predictor_68_face_landmarks.dat 放入 models/

opencv-python负责摄像头读取和画框,dlib负责检测和关键点,两个库分工明确。验证安装是否成功,跑一句python -c "import dlib, cv2; print(dlib.__version__)",能输出版本号就说明环境通了。

3.2 主循环代码:人脸检测、关键点提取与指标计算

环境准备好后,核心主循环就三件事:读帧、检测关键点、算指标。完整代码如下:

import cv2 import dlib # 初始化检测器和关键点模型 detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("models/shape_predictor_68_face_landmarks.dat") # 打开默认摄像头 cap = cv2.VideoCapture(0) 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: print("读取摄像头失败,请检查权限或被占用") break # Dlib 检测器内部按灰度图处理,但关键点模型要求RGB顺序 rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) faces = detector(rgb, 0) for face in faces: # 关键点坐标提取 landmarks = predictor(rgb, face) points = [(landmarks.part(i).x, landmarks.part(i).y) for i in range(68)] left_eye = points[36:42] right_eye = points[42:48] ear = (eye_aspect_ratio(left_eye) + eye_aspect_ratio(right_eye)) / 2.0 # 人眼可见的反馈:画人脸框和眼睛区域 cv2.rectangle(frame, (face.left(), face.top()), (face.right(), face.bottom()), (0, 255, 0), 2) for p in left_eye + right_eye: cv2.circle(frame, p, 2, (0, 0, 255), -1) cv2.imshow("Fatigue Detection", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

代码里有两个参数值得说明。detector(rgb, 0)的第二个参数是图像金字塔上采样次数,0表示不做上采样,检测速度快但对远处小脸容易漏检;改成1能多检测出小脸,代价是耗时翻倍。摄像头分辨率用640x480而不是1920x1080,因为Dlib关键点模型在高分辨率下并没有精度优势,反而帧率下降明显。眼睛区域画红点是为了调试时确认关键点是否对准,实际演示时可以选择性关闭。

3.3 眨眼计数与疲劳状态:连续帧判定才不误报

单帧EAR低于阈值不能直接判疲劳,因为眨眼本身就会让EAR短暂低于阈值。正常人眨眼持续100到300毫秒,在30帧的摄像头下大约是3到9帧;而疲劳导致的闭眼会持续2秒以上。利用这个时间差,就能区分“眨眼”和“闭眼疲劳”。

EAR_THRESH = 0.22 # EAR低于该值认为眼睛处于闭合 BLINK_MAX_FRAMES = 3 # 低于阈值但不超过3帧:算一次眨眼 CLOSED_FRAMES = 15 # 连续15帧低于阈值:判为疲劳闭眼 closed_count = 0 blink_count = 0 while True: # ... 循环内计算 ear 之后 ... if ear < EAR_THRESH: closed_count += 1 else: if closed_count > 0: if closed_count <= BLINK_MAX_FRAMES: blink_count += 1 elif closed_count >= CLOSED_FRAMES: fatigue_warning = True closed_count = 0

这个状态机的逻辑是:眼睛闭合帧数落在1到3帧区间才计一次眨眼,超过15帧判定为疲劳闭眼并触发警告。中间的4到14帧区间忽略不计——那是眨眼和闭眼之间的模糊地带,硬要判定容易翻车。实际调试时,EAR_THRESH不要照搬网上的0.22,不同摄像头角度和眼型差异会带来0.05左右的偏差,要以自己录制的测试视频为准。这个“参数玄学”问题在第五章会详细展开。

4. 给检测套上可视化界面:QThread让画面不卡的实操接线

4.1 界面选型:Tkinter省事,PyQt5更出效果

疲劳检测的成品感很大程度上来自界面。Tkinter是Python自带的GUI库,优点是不用额外装依赖、代码量少,缺点也明显:视频刷新要用label.config(image=img)反复替换图片,帧率稍微一高就卡顿丢帧,而且控件丑。如果你的毕设要求“有界面就行”,Tkinter够用。

但答辩时想让人眼前一亮,推荐PyQt5。QProgressBar画疲劳等级进度条、QLabel显示实时视频、QSlider调阈值,这些组件开箱即用,视觉效果好一个档次。PyQt5的安装也简单:pip install PyQt5。唯一要注意的是PyQt5的信号槽机制有线程约束——不能在工作线程里直接改界面控件,必须通过信号把结果发回主线程。

pip install PyQt5

这是给后面代码做铺垫的。用PyQt5做界面,建议把架构拆成三块:视频采集与检测线程、界面主线程、状态数据对象。检测线程只管算EAR和疲劳状态,通过信号发给界面刷新;界面线程不跑任何检测逻辑,只负责显示。这样分工清晰,排查问题也好定位。

4.2 QThread视频线程:界面卡顿是线程问题不是代码问题

很多初学者把检测和界面写在同一个循环里,结果发现一开摄像头界面就转圈。原因是cap.read()和Dlib检测都是阻塞操作,检测一帧几十毫秒,期间界面事件循环被卡住,窗口自然失去响应。解决办法是让检测跑在独立线程里。

from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage class VideoThread(QThread): # 信号携带QImage,主线程收到后直接setPixmap change_pixmap = pyqtSignal(QImage) update_status = pyqtSignal(float, int, int) # ear, 眨眼次数, 疲劳等级 def __init__(self): super().__init__() self.running = True def run(self): cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) blink_count = 0 fatigue_level = 0 while self.running: ret, frame = cap.read() if not ret: continue # 检测逻辑,复用第三章的主循环代码 rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) faces = detector(rgb, 0) ear = 0.3 # 默认值,实际替换为检测结果 # 将BGR帧转成QImage,注意要copy()避免缓冲区被覆盖 h, w, ch = rgb.shape bytes_per_line = ch * w qt_img = QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888).copy() self.change_pixmap.emit(qt_img) self.update_status.emit(ear, blink_count, fatigue_level) cap.release() def stop(self): self.running = False self.wait()

这段代码的要点有三个。第一,信号里只传QImage而不是numpy数组,因为numpy跨线程传递要复制一整块内存,耗时高还容易踩浅拷贝的坑;QImage.copy()保证传给界面的图像缓冲区独立,防止主线程还没来得及显示就被下一帧覆盖。第二,self.running作为线程退出标志,关闭窗口时调用stop()干净释放摄像头,否则下次启动程序会报摄像头被占用。第三,ear、blink_count这些数据通过自定义信号更新,界面只做显示不参与计算。

4.3 状态面板与统计数值:把疲劳等级变成看得懂的进度条

界面布局按“左侧视频、右侧数据”设计。左半部分放QLabel显示实时视频画面,右半部分从上到下依次放:当前状态标签(正常/疲劳/危险)、疲劳等级进度条、眨眼次数、哈欠次数、持续运行时长。疲劳等级用0到3表示:

  • 0级:EAR正常,无异常,绿色进度条
  • 1级:偶发闭眼帧数达到阈值,黄色提醒
  • 2级:检测到打哈欠或短时闭眼,橙色警告
  • 3级:连续闭眼超时,红色报警
from PyQt5.QtWidgets import QProgressBar class FatiguePanel(QProgressBar): def __init__(self): super().__init__() self.setRange(0, 3) self.setValue(0) self.setStyleSheet("QProgressBar::chunk { background-color: #2ecc71; }") def update_level(self, level): self.setValue(level) colors = {0: "#2ecc71", 1: "#f39c12", 2: "#e67e22", 3: "#e74c3c"} self.setStyleSheet( f"QProgressBar::chunk {{ background-color: {colors[level]}; }}" )

进度条颜色随等级变化,比单纯显示数字直观得多。界面线程收到update_status信号后,同步更新进度条和计数标签。这里有个细节:计数标签不要每次整数字符串刷新,只在数值变化时更新,能减少无效的界面重绘。

5. 疲劳检测避坑指南:装库失败、模型加载慢与阈值玄学的解法

5.1 Dlib编译失败:先装好C++工具链再谈pip install

  • 现象:pip install dlib卡在Building wheel for dlib,几分钟后报错,日志里有fatal error C1083或g++: command not found。
  • 原因:Dlib从源码编译需要C++编译器和CMake。Windows下缺Visual Studio Build Tools,Linux下缺build-essential和cmake。pip在下载源码包后尝试现场编译,环境不满足就直接失败。
  • 解决:Windows用户先装Visual Studio Build Tools,勾选“使用C++的桌面开发”工作负载;Linux用户先执行sudo apt install build-essential cmake。懒得折腾编译链的话,用Anaconda执行conda install -c conda-forge dlib,预编译包直接装好。装完验证import dlib不报错再继续。

5.2 模型文件下载慢、加载卡:路径、格式与内存三个坑

  • 现象:shape_predictor_68_face_landmarks.dat加载要十几秒,或者加载后程序直接闪退。
  • 原因:模型文件约百兆,加载本身就是I/O密集操作,首次加载慢正常;但如果用相对路径,IDE工作目录和命令行不一致会导致找不到文件;更隐蔽的坑是模型文件存放的路径含中文,Dlib底层用C++文件流读取,对中文路径兼容性不好。
  • 解决:项目内创建models目录,用os.path.abspath(__file__)定位文件所在目录再拼接模型路径,保证任何入口执行都能找到。路径里不要有中文。加载模型放到程序初始化阶段,不要放进每帧循环里——那会卡到天荒地老。如果内存紧张(4GB以下的机器),检测前先关闭其他大软件,Dlib加载模型后占用内存约几百MB,在可接受范围。

5.3 戴眼镜、侧脸和逆光:关键点飘了的处理办法

  • 现象:检测画面里人脸框在,但眼睛关键点落在镜框边缘或眉毛上,EAR值忽高忽低,眨眼计数一秒钟跳好几次。
  • 原因:68点模型是在正脸、光照均匀的数据集上训练的。戴眼镜时镜框的强边缘会干扰关键点回归;侧脸超过30度时部分轮廓点本来就被遮挡;逆光时面部灰度对比度低,检测器容易把人脸框偏移。
  • 解决:三管齐下。第一,detector(rgb, 1)做一次上采样,提高小脸和偏转脸检出率,代价是帧率下降,建议只在光线差的环境开启。第二,对EAR值做合理性检查——如果连续多帧EAR都大于0.5或小于0.01,说明关键点大概率飘了,直接丢弃该帧不参与计数。第三,界面上加一行“请正对摄像头”的文字提示,让用户自己调整位置,比代码硬扛更实际。

5.4 阈值玄学:用开机基线自动适配不同人眼型

  • 现象:同一套代码,A同学测试时眨眼识别很准,换B同学上去频繁误报——眼睛还没闭就被记了一次眨眼,或者睁着眼被判成疲劳。
  • 原因:EAR的绝对值和个人眼型、摄像头安装高度、人脸距离都相关。单眼皮和双眼皮、眼窝深浅都会让EAR的正常区间偏移0.05到0.1。网上下载的代码里写死的0.22只是作者自己测试出来的经验值,不是通用参数。
  • 解决:程序启动后前30帧不判断状态,只收集EAR值作为个人基线,然后按基线的75%动态设定阈值。
baseline_ears = [] ear_thresh = 0.22 # 默认值,后续被基线覆盖 while True: # ... 计算 ear 之后 ... if len(baseline_ears) < 30: baseline_ears.append(ear) continue # 前30帧跳过判定 if len(baseline_ears) == 30: baseline = sum(baseline_ears) / len(baseline_ears) ear_thresh = baseline * 0.75 baseline_ears.append(-1) # 标志位,避免重复计算 # 正常判定逻辑 if ear < ear_thresh: # ...

前30帧大约占用1秒(30fps),用户坐定后很快完成标定,不影响使用体验。基线只取一次,不要持续更新——否则用户在疲劳时EAR连续偏低,基线会被带低,阈值跟着失效。这个自适应方案能解决80%的“换个用户就失灵”问题,剩下20%靠界面上的手动阈值滑杆补足。

6. 答辩前把演示做扎实:视频回放、参数滑杆与检测报告

6.1 离线视频回放让答辩不再依赖现场摄像头

现场用摄像头演示最容易翻车:教室投影仪挡光、电脑摄像头分辨率低、USB摄像头驱动装不上。我一般会在功能里加一个“本地视频模式”——把cv2.VideoCapture(0)换成cv2.VideoCapture("test.mp4"),检测和判定逻辑完全复用,只是视频源不同。答辩时先放一段自己提前录好的测试视频,把眨眼、打哈欠、点头的动作完整展示一遍,再用实时摄像头做补充演示,两条路都走得通。

录测试视频有讲究:找光线均匀的室内环境,正对摄像头坐好,依次做“正常平视30秒、连续眨眼10次、张大嘴打哈欠、连续低头点头”四个动作,每个动作之间停顿几秒。这样检测结果里有明显的状态切换,答辩讲解时能对着画面说“现在EAR降到阈值以下,眨眼计数加一”。

6.2 参数滑杆和检测报告:把“调参”变成可展示的数据

加一个QSlider控件手动调EAR_THRESH,范围0.1到0.4,滑块移动时实时更新全局阈值。这个设计的妙处在于:答辩老师可能会问“你这个阈值设多少,为什么是这么多”,你可以现场拖动滑杆演示——阈值调高时误报变多,调低时疲劳检测变迟钝,用可视化的方式把这个“玄学”问题讲成参数敏感性分析,反而比照本宣科更有说服力。

检测报告建议导出CSV,表格结构如下:

时间戳EARMAR头部角度眨眼计数疲劳等级
00:00:010.280.353.200
00:00:020.110.322.810
00:00:030.290.584.111

每秒记一行,程序退出后写入report.csv。用这个数据可以算一个简化版PERCLOS指标——闭眼帧数占总帧数的比例,这个比例超过0.4就说明测试者在当前时段确实处于疲劳状态。导出之后,想画趋势曲线接一段python数据分析与可视化的代码就能出图,整个毕设的数据支撑就闭环了。

我做这个项目时最大的教训是:不要等到最后一周才录演示视频,更不要拿别人的开箱即用代码直接答辩——摄像头型号差一档,阈值就不一样。先用自己的脸把参数标定好,再录一段完整演示流程,最后才写界面。顺序反了,后面全是补窟窿。希望这个方案能帮你少踩几个坑,把时间花在真正有区分度的界面和数据展示上。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询