简介:这份资源是一套基于Python开发的驾驶员面部特征疲劳检测系统源码,面向计算机相关专业学生、毕业设计选题者以及需要人脸识别与疲劳状态判断方案的开发者。系统通过摄像头采集驾驶员面部信息,识别其当前疲劳程度并判断是否需要休息,可迁移至交警监控、高速收费站等场景,用于排查疲劳驾驶行为。压缩包共24个文件,约68.33MB,包含5个py核心脚本、10个xml配置与界面文件、1个dat模型数据、1个mp3提示音、1个docx说明文档及README等,覆盖从检测逻辑到界面配置的完整工程结构,下载后可直接运行调试。目前已有1358人学习下载,说明该方案在毕业设计群体中具备一定参考价值。读者可借此快速理解人脸识别与疲劳检测的工程实现路径,参考其模块划分与配置方式,用于课程设计、毕设开发或二次功能扩展。
1. 从一张摄像头截图说起:这套疲劳检测源码到底能跑出什么
你打开摄像头,画面里是自己那张熬夜脸。程序在左上角画了个框,标出眼睛和嘴巴的位置,旁边跳着两个数字:EAR 0.21、MAR 0.58。三秒后,屏幕弹出一行红字——"疲劳,请休息"。这不是什么玄学,就是一套基于面部特征几何关系的判定逻辑。这套 Python 疲劳检测系统源码,核心干的事就三件:用 dlib 或 OpenCV 抓人脸 68 个关键点,从关键点里算出眼睛纵横比和嘴巴纵横比,再用阈值加连续帧计数判断你是不是在打哈欠或闭眼。它适合谁?做毕业设计需要完整可跑工程的学生、想快速搭一个视觉预警原型的开发者、以及手头有摄像头想验证 EAR/MAR 这套经典方案到底灵不灵的工程师。源码包里通常包含检测脚本、预训练模型文件、依赖清单和一份说明文档,拿到手改改阈值就能用。
2. 环境搭不起来,后面全是空谈:依赖安装与模型文件放置
2.1 为什么 dlib 是第一个拦路虎
这套源码的检测精度高度依赖 dlib 的 68 点人脸关键点模型。dlib 本身是 C++ 写的,Python 绑定在 Windows 上编译需要 CMake 和 Visual Studio Build Tools,很多人卡在pip install dlib直接报错。常见做法是两条路:一是用 conda 装预编译版本,二是直接下 whl 文件本地安装。我一般会先确认 Python 版本,3.8 到 3.10 兼容性最好,3.11 以上部分 whl 还没跟上。
# 先看 Python 版本,别用太新的 python --version # 用 conda 装 dlib 最省事,它会自动处理编译依赖 conda install -c conda-forge dlib # 如果坚持用 pip,先装 CMake 和编译工具 pip install cmake pip install dlib上面第一段是版本确认,第二段走 conda 渠道,第三段是 pip 路线。参数上没什么可调的,关键是别在没装 CMake 的情况下硬 pip,报错信息里出现CMake must be installed就是这个问题。装完 dlib 后,OpenCV 反而简单,pip install opencv-python基本一把过。numpy 和 scipy 通常作为依赖自动带上,但如果你用的是老版本源码,可能锁定了特定版本,比如numpy==1.21,这时候别盲目升级。
2.2 模型文件放错位置,程序静默失败
源码包里一般有个shape_predictor_68_face_landmarks.dat文件,大小在 95MB 左右。这个文件不放对位置,程序不会报错说"找不到模型",而是直接在某一行抛异常或者干脆检测不到人脸。我见过最坑的情况是:代码里写的是相对路径./models/shape_predictor_68_face_landmarks.dat,但压缩包解压后文件在根目录,跑起来摄像头亮了但一个框都不画。
import dlib import os # 先确认模型文件到底在哪 model_path = "shape_predictor_68_face_landmarks.dat" if not os.path.exists(model_path): # 别猜,直接打印当前工作目录 print("当前目录:", os.getcwd()) print("目录下文件:", os.listdir(".")) raise FileNotFoundError("模型文件不在当前目录,检查解压路径") detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor(model_path)这段代码先做存在性检查,再初始化检测器和预测器。参数说明:get_frontal_face_detector()用的是 HOG 特征加 SVM 的人脸检测器,速度比 CNN 版快,CPU 上也能跑;shape_predictor加载的就是那个 95MB 的模型。如果你把模型放在models/子目录,路径就要写成models/shape_predictor_68_face_landmarks.dat。注意 Windows 下路径分隔符用/或\\都行,但别混用。
2.3 摄像头权限与分辨率设置
笔记本自带摄像头一般索引是 0,外接 USB 摄像头可能是 1 或 2。源码里通常写死cv2.VideoCapture(0),如果你插了外接摄像头但画面是黑的,先换成 1 试试。分辨率方面,默认 640x480 足够跑 68 点检测,调到 1080p 反而拖慢帧率。
import cv2 cap = cv2.VideoCapture(0) # 设置分辨率,别太高,检测帧率会掉 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) if not cap.isOpened(): print("摄像头没打开,检查索引或驱动") exit() while True: ret, frame = cap.read() if not ret: break # 这里后续接人脸检测逻辑 cv2.imshow("Frame", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()VideoCapture(0)里的 0 是摄像头索引,set两个参数分别控制宽高。waitKey(1)里的 1 是毫秒数,太小 CPU 占用高,太大画面卡顿,1 到 30 之间调。按 q 退出是惯例,别改成 ESC 又忘了写键值。
3. EAR 和 MAR 怎么算:从 68 个点到疲劳判定的完整链路
3.1 眼睛纵横比 EAR 的几何含义
dlib 的 68 点模型里,左眼是 36 到 41 号点,右眼是 42 到 47 号点。EAR 的计算方式是用眼睛纵向距离之和除以横向距离的两倍。眼睛睁开时 EAR 在 0.25 到 0.35 之间,闭眼时会掉到 0.15 以下。这个阈值不是固定的,戴眼镜、眯眼、光线变化都会影响,所以源码里通常会让你自己调。
from scipy.spatial import distance as dist def eye_aspect_ratio(eye): # 纵向距离:上下眼睑两组点 A = dist.euclidean(eye[1], eye[5]) B = dist.euclidean(eye[2], eye[4]) # 横向距离:左右眼角 C = dist.euclidean(eye[0], eye[3]) # EAR 公式 ear = (A + B) / (2.0 * C) return ear # 假设 landmarks 是 dlib 返回的 68 点 left_eye = [(landmarks.part(i).x, landmarks.part(i).y) for i in range(36, 42)] right_eye = [(landmarks.part(i).x, landmarks.part(i).y) for i in range(42, 48)] left_ear = eye_aspect_ratio(left_eye) right_ear = eye_aspect_ratio(right_eye) ear = (left_ear + right_ear) / 2.0dist.euclidean算的是两点欧氏距离。A 和 B 是纵向两组,C 是横向一组。分母乘 2.0 是为了归一化,让不同人脸尺寸下的 EAR 可比。左右眼各算一次再取平均,避免单眼遮挡导致误判。参数上唯一要调的是后面判定用的阈值,常见起始值是 0.25。
3.2 嘴巴纵横比 MAR 与打哈欠检测
嘴巴的关键点是 48 到 67,其中 48 到 54 是外唇,60 到 67 是内唇。打哈欠时嘴巴张开,纵向距离变大,MAR 会从正常的 0.1 到 0.3 跳到 0.5 以上。但说话也会让 MAR 波动,所以不能只看单帧,要结合连续帧数。
def mouth_aspect_ratio(mouth): # 用外唇上下中点算纵向 A = dist.euclidean(mouth[2], mouth[10]) # 51 和 59 B = dist.euclidean(mouth[4], mouth[8]) # 53 和 57 # 横向用嘴角 C = dist.euclidean(mouth[0], mouth[6]) # 48 和 54 mar = (A + B) / (2.0 * C) return mar # 取 48 到 68 号点 mouth = [(landmarks.part(i).x, landmarks.part(i).y) for i in range(48, 68)] mar = mouth_aspect_ratio(mouth)这里索引和眼睛不同,mouth[2]对应的是 50 号点,mouth[10]对应 58 号点,具体取决于你取的列表范围。源码里如果直接写range(48, 68),那mouth[0]就是 48 号点。MAR 阈值常见起始值是 0.6,但每个人嘴型不同,建议先跑一遍正常说话和打哈欠的对比。
3.3 连续帧计数与报警逻辑
单帧 EAR 低不代表疲劳,可能只是眨眼。所以源码里一般会设一个EYE_AR_THRESH和一个EYE_AR_CONSEC_FRAMES,比如连续 3 帧 EAR 低于 0.25 才计数,计数超过 20 帧才报警。MAR 同理,连续 5 帧高于 0.6 算一次哈欠。
EYE_AR_THRESH = 0.25 EYE_AR_CONSEC_FRAMES = 20 MOUTH_AR_THRESH = 0.6 MOUTH_AR_CONSEC_FRAMES = 5 eye_counter = 0 mouth_counter = 0 fatigue_alarm = False # 在视频循环里 if ear < EYE_AR_THRESH: eye_counter += 1 if eye_counter >= EYE_AR_CONSEC_FRAMES: fatigue_alarm = True else: eye_counter = 0 if mar > MOUTH_AR_THRESH: mouth_counter += 1 if mouth_counter >= MOUTH_AR_CONSEC_FRAMES: fatigue_alarm = True else: mouth_counter = 0EYE_AR_CONSEC_FRAMES设 20 意味着在 30fps 下约 0.67 秒闭眼才报警,这个值太小会频繁误报,太大又反应迟钝。MOUTH_AR_CONSEC_FRAMES设 5 是防止说话时嘴角抽动触发。fatigue_alarm一旦置 True,你可以选择画红框、响蜂鸣器或者写日志。注意计数器在条件不满足时要归零,否则会累积误判。
4. 避坑与排查:跑通这套源码最常见的五个翻车点
4.1 现象:摄像头亮了但一个框都不画
原因通常是模型文件路径不对,或者 dlib 检测器在低光照下失效。先确认shape_predictor_68_face_landmarks.dat和脚本在同一目录,或者代码里的路径指向正确。如果路径没问题,把画面亮度调高,dlib 的 HOG 检测器对暗光很敏感。解决方法是加一行cv2.convertScaleAbs(frame, alpha=1.5, beta=30)提亮,或者换用 CNN 检测器(速度慢但鲁棒)。
4.2 现象:EAR 值一直在 0.3 以上,闭眼也不降
原因可能是关键点索引取错了。dlib 的 68 点模型里,左眼是 36 到 41,右眼是 42 到 47,但有些源码为了省事只取单眼或者索引偏移。检查你的range起点和终点,打印出每个点的坐标画在图上确认。另一个可能是摄像头角度太偏,侧脸导致关键点漂移。解决方法是正对摄像头,或者加一个人脸对齐步骤。
4.3 现象:程序跑几秒就卡死或内存暴涨
原因通常是视频循环里没有释放帧,或者cv2.imshow没有配waitKey。每读一帧都要cap.read(),处理完要cv2.imshow再waitKey。如果用了多线程,注意 dlib 的 predictor 不是线程安全的。解决方法是确保循环里有waitKey(1),并且不要在循环里反复创建 detector 和 predictor 对象,提到循环外面初始化。
4.4 现象:报警太频繁,正常眨眼也触发
原因是EYE_AR_CONSEC_FRAMES设太小,或者EYE_AR_THRESH设太高。正常眨眼 EAR 会短暂掉到 0.2 以下,但只持续 1 到 2 帧。把连续帧阈值提到 15 到 20,阈值降到 0.22 左右试试。另外不同人眼型差异大,戴眼镜的人 EAR 基线偏低,需要单独校准。解决方法是加一个校准模式,让用户正常睁眼 5 秒取平均 EAR 作为基准。
4.5 现象:MAR 一直很高,不说话也报警
原因是嘴巴关键点取到了下巴或者鼻子区域。检查range(48, 68)是否被改成了range(48, 60)之类。另外打哈欠和说话的区别在于持续时间,说话时 MAR 波动快,打哈欠是持续张开。把MOUTH_AR_CONSEC_FRAMES提到 10 以上,并且要求 MAR 连续高于阈值才计数。如果还是误报,可以结合 EAR 一起判断,只有 EAR 低且 MAR 高才报疲劳。
5. 进阶技巧:把检测结果写进日志并用 matplotlib 画疲劳曲线
跑通基础检测后,我习惯把每帧的 EAR 和 MAR 存下来,事后用 matplotlib 画一条时间曲线。这样能直观看到阈值设得合不合理,也能给毕业设计论文提供图表素材。下面这段代码在原有循环里加一个列表收集数据,退出后画图。
import matplotlib.pyplot as plt ear_history = [] mar_history = [] frame_count = 0 # 在视频循环里,每帧 append ear_history.append(ear) mar_history.append(mar) frame_count += 1 # 循环结束后 plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(ear_history, label="EAR") plt.axhline(y=0.25, color='r', linestyle='--', label="阈值 0.25") plt.xlabel("帧数") plt.ylabel("EAR") plt.legend() plt.subplot(1, 2, 2) plt.plot(mar_history, label="MAR", color='orange') plt.axhline(y=0.6, color='r', linestyle='--', label="阈值 0.6") plt.xlabel("帧数") plt.ylabel("MAR") plt.legend() plt.tight_layout() plt.savefig("fatigue_curve.png", dpi=150) plt.show()ear_history和mar_history是普通列表,每帧追加一次,30fps 跑 10 分钟也就 18000 个点,内存完全扛得住。axhline画的是阈值参考线,红色虚线。savefig的dpi=150保证论文插图清晰。如果你要分析特定时间段,可以在列表里同时存时间戳,横轴换成秒数。
另一个进阶方向是把报警逻辑从"单帧判定"改成"滑动窗口投票"。比如取最近 30 帧,如果超过 20 帧 EAR 低于阈值才报警,这样比连续帧计数更抗抖动。实现上用一个collections.deque(maxlen=30),每帧 append 一个布尔值,然后sum(window) > 20触发。这个改动不大,但误报率会明显下降。
还有一个我踩过的坑:dlib 的 68 点模型对戴眼镜的人反光很敏感,镜片上的光斑会让关键点跳到镜框上。解决办法是加一个预处理,用cv2.equalizeHist做直方图均衡,或者让用户摘掉眼镜。如果必须戴眼镜,考虑换用 MediaPipe 的 Face Mesh,它输出 468 个点,对眼镜的鲁棒性更好,但那是另一套依赖了。
从那以后我每次拿到新的视觉检测源码,都强制先跑一遍模型文件路径检查和摄像头索引确认,再去看算法逻辑。这两步不过,后面调参全是白费。希望帮到你。
本文还有配套的精品资源,点击获取