简介基于Python与Dlib的驾驶员疲劳检测系统围绕打哈欠、眨眼、瞌睡点头三个典型疲劳特征通过人脸朝向、瞳孔开合度、眨眼频率等实时计算驾驶员注意力集中程度并在疲劳迹象出现时及时提示。系统配有可视化界面可用于毕业设计、课程设计或实际场景演示适合计算机、人工智能、自动化、电子信息等专业学生及开发者参考学习。压缩包共17个文件体积约85.92MB包含核心Python源码眼睛检测、嘴巴检测、点头检测模块、Dlib人脸68点特征模型、Jupyter演示脚本、可视化界面文件、安装配置工具、运行效果图及测试视频等目录结构清晰便于按模块查看与调试附带的人脸模型与安装工具可降低环境配置门槛。已有232人学习下载方案在疲劳检测类题目中具有一定参考价值。除可直接运行的完整代码外还配有README说明、界面设计源文件与演示视频可帮助读者快速掌握从算法到界面的实现思路稍加修改即可扩展为独立作品适合作为毕业设计基础进行二次开发。1. 用Python和Dlib做疲劳检测为什么毕设都选这条路线你可能已经翻过不少开源项目发现用深度学习做疲劳检测的demo很多但真正敢拿到答辩现场跑的很少。原因很简单深度学习方案要训练数据、要标注、要GPU而大部分毕设现场只有一台笔记本和一个USB摄像头。Dlib这个老牌C库配合Python走的是另一条路——用68个预训练人脸关键点把眨眼、打哈欠、点头这些动作换算成几个几何比例值不训练模型CPU实时跑。这就是“基于PythonDlib的驾驶员疲劳检测”在毕业设计里经久不衰的原因工作量可控、原理能讲透、界面演示直观还自带一套可复现的评价指标。这个方案本质上是把“疲劳”量化成眼睛纵横比、嘴部纵横比和头部角度三个数字再结合连续帧判定输出状态。它能解决的核心问题是在不依赖昂贵设备的前提下用普通摄像头完成眨眼计数、哈欠识别、瞌睡点头检测并落到可视化界面上。适合正在做毕设、想在CV方向快速出成果的学生也适合想拿现成代码做二次开发的入门工程师。2. Dlib的68个关键点把“眨眼”和“哈欠”换算成两个数字2.1 为什么选Dlib而不是OpenCV人脸检测或深度学习模型做疲劳检测第一件要决定的事是人脸检测和关键点提取用什么。OpenCV自带的Haar级联也能框出人脸但它只给你一个矩形框拿不到眼睛和嘴巴的坐标后续的疲劳判断无从谈起。深度学习模型倒是有现成的关键点输出比如OpenCV的DNNS模块和MediaPipe但打包体积大推理速度在纯CPU环境下也吃紧。Dlib的位置刚好卡在中间。dlib.get_frontal_face_detector()用的是HOG特征加线性SVM分类器对正脸检测速度极快关键点提取用的是ERTEnsemble 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年一篇用面部关键点做实时眨眼检测的论文提出了EAREye 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 python3.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 ToolsWindows或build-essentialLinux再重试。用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(imageimg)反复替换图片帧率稍微一高就卡顿丢帧而且控件丑。如果你的毕设要求“有界面就行”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( fQProgressBar::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 ToolsLinux下缺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.20000:00:020.110.322.81000:00:030.290.584.111每秒记一行程序退出后写入report.csv。用这个数据可以算一个简化版PERCLOS指标——闭眼帧数占总帧数的比例这个比例超过0.4就说明测试者在当前时段确实处于疲劳状态。导出之后想画趋势曲线接一段python数据分析与可视化的代码就能出图整个毕设的数据支撑就闭环了。我做这个项目时最大的教训是不要等到最后一周才录演示视频更不要拿别人的开箱即用代码直接答辩——摄像头型号差一档阈值就不一样。先用自己的脸把参数标定好再录一段完整演示流程最后才写界面。顺序反了后面全是补窟窿。希望这个方案能帮你少踩几个坑把时间花在真正有区分度的界面和数据展示上。本文还有配套的精品资源点击获取