简介:这份资源是面向计算机相关专业毕业设计、课程设计与项目实战学习者的完整项目包,主题为基于卷积神经网络的人脸识别驾驶员疲劳检测与预警系统。项目经导师指导并认可,可直接作为毕设或期末大作业使用,代码经过严格调试,确保能够运行。压缩包共19个文件,约78.33MB,以Python源码为主,包含11个py脚本,另有xml级联分类器文件、txt运行说明与依赖清单、hdf5模型权重、exe可执行程序及md说明文档,覆盖从数据处理、模型训练到界面交互的完整链路。内容预览显示项目含人脸检测、眼睛定位、CNN模型构建与评估、Tkinter可视化界面等模块,并附带训练好的mini_XCEPTION模型文件,便于直接复现检测效果。目前已有244人学习下载,适合希望快速掌握疲劳检测算法实现、积累深度学习项目经验的学习者参考与二次开发。
1. 从一张打哈欠的抓拍说起:这套疲劳检测系统到底在做什么
凌晨两点跑长途的司机被车内摄像头抓拍到连续打哈欠,系统在 1.5 秒内判定疲劳并触发蜂鸣预警——这就是基于卷积神经网络的人脸识别驾驶员疲劳检测与预警系统要解决的真实问题。它不依赖方向盘扭矩、不依赖车道偏移,只靠一张脸,把眼睛闭合、嘴巴张合这些视觉信号翻译成"该休息了"的判断。整套方案用 Python 落地,核心是 CNN 卷积神经网络做人脸关键点回归,再叠加 EAR、MAR 两个几何指标做疲劳判定,最后接一个预警模块。适合做毕业设计、课程设计,也适合想入门计算机视觉的 Python 学习者——你不需要 GPU 集群,一台带普通摄像头的笔记本就能跑通全流程。下面我把这套系统从环境搭建到预警触发,按能复现的顺序拆开讲。
2. 疲劳检测的技术选型:为什么是 CNN 而不是传统特征
2.1 从 Haar 到 CNN:人脸检测这一步怎么选
做疲劳检测,第一步永远是把人脸从画面里框出来。很多教程一上来就用 OpenCV 自带的 Haar 级联分类器,代码三行就能跑,但实际用起来问题不少:侧脸、戴眼镜、光线偏暗时漏检率明显上升,司机开车时头部本来就会转动,Haar 的鲁棒性撑不住。常见做法是换成基于 CNN 的人脸检测器,比如 OpenCV 的 DNN 模块加载 Caffe 模型,或者直接用 dlib 的 HOG + CNN 检测器。我一般会选 dlib 的 68 点模型,原因是它同时输出人脸框和 68 个关键点,一步到位,省掉单独做关键点检测的环节。
选型逻辑其实很简单:疲劳判定的精度上限,取决于关键点定位的稳定性。Haar 只给框,关键点还得另找方案;dlib 的 68 点模型在正脸和轻微侧脸下误差能控制在 3 像素以内,对 EAR/MAR 这种比值型指标来说完全够用。代价是 dlib 的 CNN 检测器比 HOG 慢,但在 640×480 分辨率、单张人脸场景下,普通 CPU 也能跑到 15 FPS 以上,实时性没问题。
2.2 EAR 和 MAR:把"困"翻译成两个数字
关键点拿到之后,怎么判断疲劳?业界最通用的两个指标是 EAR(Eye Aspect Ratio,眼睛纵横比)和 MAR(Mouth Aspect Ratio,嘴巴纵横比)。EAR 的思路是:眼睛睁开时,上下眼睑的垂直距离和左右眼角的水平距离之比维持在一个稳定区间;闭眼时垂直距离骤降,比值跟着掉。公式用 68 点模型里的 6 个眼部点就能算:
import numpy as np def eye_aspect_ratio(eye_points): # eye_points: 6 个 (x, y) 坐标,顺序为 [左眼角, 上左, 上右, 右眼角, 下右, 下左] # 垂直距离:上眼睑两点与下眼睑两点的欧氏距离 vertical_1 = np.linalg.norm(eye_points[1] - eye_points[5]) vertical_2 = np.linalg.norm(eye_points[2] - eye_points[4]) # 水平距离:左右眼角的欧氏距离 horizontal = np.linalg.norm(eye_points[0] - eye_points[3]) ear = (vertical_1 + vertical_2) / (2.0 * horizontal) return ear逻辑说明:分子取两组垂直距离的平均,是为了抵消单点抖动;分母用水平距离做归一化,这样人脸远近变化时 EAR 值不会跟着漂。参数上,正常人睁眼 EAR 在 0.25~0.35 之间,闭眼会掉到 0.15 以下。阈值不能拍脑袋定,我一般先跑一段自己录的视频,统计睁眼帧的 EAR 均值和标准差,取均值减 2 倍标准差作为闭眼阈值,这样比固定 0.2 稳得多。
MAR 的计算方式类似,用嘴巴的 8 个点,取上下唇垂直距离除以左右嘴角水平距离。打哈欠时 MAR 会从正常的 0.2 左右飙到 0.6 以上。注意 MAR 的阈值个体差异比 EAR 大,有人天生嘴型宽,建议同样用统计法标定。
2.3 为什么不用端到端 CNN 直接分类疲劳
有人会问:既然都上 CNN 了,为什么不直接训一个二分类网络,输入人脸图,输出"疲劳/清醒"?这条路我试过,翻车点在于数据。端到端分类需要大量标注好的疲劳/非疲劳人脸,而公开数据集里真正标注疲劳状态的很少,大部分只有打哈欠、闭眼这类动作标签,且场景单一。自己标数据成本高,模型还容易过拟合到某个人的脸型。相比之下,关键点回归 + 几何指标的方案,中间结果可解释、阈值可调、换个人只需重新标定阈值,工程上更可控。毕业设计场景下,这套组合拳的性价比明显更高。
3. 用 Python 把检测流程跑通:从摄像头到 EAR 曲线
3.1 环境搭建与依赖安装
先把环境弄干净。Python 版本建议 3.8~3.10,太新的版本有些 CV 库轮子还没跟上。用 conda 或 venv 建虚拟环境都行,我习惯 venv:
python -m venv fatigue_env # Windows 激活 fatigue_env\Scripts\activate # Linux / macOS 激活 source fatigue_env/bin/activate pip install opencv-python dlib numpy scipy imutils参数说明:opencv-python 负责读摄像头和图像处理;dlib 提供 68 点关键点模型,注意 dlib 在 Windows 上直接 pip 装可能编译失败,稳妥做法是去下载对应 Python 版本的 .whl 文件再 pip install;scipy 用来做滤波,后面平滑 EAR 曲线要用;imutils 是常用图像工具集,可选但省事。装完跑一句python -c "import cv2, dlib; print(cv2.__version__, dlib.__version__)"验证,能打印版本号就说明环境通了。
3.2 关键点提取与 EAR/MAR 实时计算
dlib 的 68 点模型需要单独下载shape_predictor_68_face_landmarks.dat,这个文件约 100MB,放到项目目录下。下面是把摄像头帧转成 EAR/MAR 数值的核心循环:
import cv2 import dlib import numpy as np from scipy.spatial import distance as dist detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") # 68 点索引:左眼 36-41,右眼 42-47,嘴巴 48-67 LEFT_EYE = list(range(36, 42)) RIGHT_EYE = list(range(42, 48)) MOUTH = list(range(48, 68)) def get_ear_mar(shape): # 把 dlib 的 shape 对象转成 numpy 数组 pts = np.array([[shape.part(i).x, shape.part(i).y] for i in range(68)]) left_ear = eye_aspect_ratio(pts[LEFT_EYE]) right_ear = eye_aspect_ratio(pts[RIGHT_EYE]) ear = (left_ear + right_ear) / 2.0 # MAR:上下唇垂直距离 / 嘴角水平距离 mouth_vertical = dist.euclidean(pts[62], pts[66]) mouth_horizontal = dist.euclidean(pts[60], pts[64]) mar = mouth_vertical / mouth_horizontal return ear, mar cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 0) for face in faces: shape = predictor(gray, face) ear, mar = get_ear_mar(shape) cv2.putText(frame, f"EAR:{ear:.2f} MAR:{mar:.2f}", (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow("Fatigue Detection", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()逻辑说明:detector(gray, 0)里的 0 表示不上采样,速度快但小脸可能漏检,如果人脸离摄像头远可以改成 1。get_ear_mar把 dlib 的 shape 对象转成 numpy 数组再切片,比逐个 part 调用快。参数上,左右眼 EAR 取平均能抵消单侧遮挡;MAR 用 62/66 和 60/64 这两组点,是 68 点模型里上下唇和嘴角的标准索引,别用错。跑起来后对着摄像头睁眼闭眼,看 EAR 数值是否在 0.3 和 0.15 之间跳变,跳变不明显就检查关键点有没有标歪。
3.3 用滑动窗口和滤波压住抖动
直接拿单帧 EAR 做判断,会有一个血泪经验:关键点每帧都有微小抖动,EAR 曲线毛刺很多,偶尔一帧掉到阈值以下就误报。解决办法是两层处理。第一层用滑动窗口平均,取最近 N 帧的 EAR 均值;第二层用一维卡尔曼滤波或简单的指数平滑。我一般用窗口长度 5、指数平滑系数 0.3 的组合,既能压抖动又不至于把真实的闭眼信号也抹平。
from collections import deque ear_window = deque(maxlen=5) alpha = 0.3 smoothed_ear = None def smooth_ear(raw_ear): global smoothed_ear ear_window.append(raw_ear) window_avg = sum(ear_window) / len(ear_window) if smoothed_ear is None: smoothed_ear = window_avg else: smoothed_ear = alpha * window_avg + (1 - alpha) * smoothed_ear return smoothed_ear参数说明:maxlen=5对应约 0.3 秒的窗口(按 15 FPS 算),太短压不住抖动,太长会延迟预警;alpha=0.3表示新值权重,越小越平滑但响应越慢。这两个值要根据实际帧率调,帧率低就加大窗口。判断闭眼时不要用瞬时值,用平滑后的值连续超过阈值若干帧才计数,这个"连续帧数"是抗误报的关键。
4. 疲劳判定与预警:阈值、计数和触发逻辑
4.1 闭眼与打哈欠的判定规则
有了平滑后的 EAR/MAR,判定规则要分两条线走。闭眼线:EAR 连续低于阈值超过 M 帧,判定为一次闭眼事件;如果闭眼持续超过 2 秒(约 30 帧),直接触发疲劳预警,因为正常眨眼不会闭这么久。打哈欠线:MAR 连续高于阈值超过 N 帧,判定为一次哈欠,统计最近 60 秒内的哈欠次数,超过 3 次触发预警。两条线是"或"的关系,任一满足就报警。
EAR_THRESHOLD = 0.20 # 需按 3.2 的统计法重新标定 MAR_THRESHOLD = 0.55 CONSEC_FRAMES = 3 # 连续帧数,抗单帧抖动 DROWSY_SECONDS = 2.0 FPS_ESTIMATE = 15 eye_counter = 0 yawn_counter = 0 drowsy_frames = int(DROWSY_SECONDS * FPS_ESTIMATE) def judge(ear, mar): global eye_counter, yawn_counter if ear < EAR_THRESHOLD: eye_counter += 1 else: if eye_counter >= CONSEC_FRAMES: pass # 一次正常眨眼,可记录 eye_counter = 0 if mar > MAR_THRESHOLD: yawn_counter += 1 else: yawn_counter = 0 if eye_counter >= drowsy_frames: return "DROWSY_EYE" if yawn_counter >= int(1.5 * FPS_ESTIMATE): return "DROWSY_YAWN" return "NORMAL"逻辑说明:eye_counter在 EAR 低于阈值时累加,回升即清零,这样只有连续闭眼才会累积到drowsy_frames。yawn_counter同理,一次哈欠持续 1.5 秒以上才计数。参数上,EAR_THRESHOLD和MAR_THRESHOLD必须按 3.2 的统计法标定,直接抄 0.20/0.55 只能算起点;FPS_ESTIMATE最好实测,用time.time()算实际帧率再代入,否则drowsy_frames会偏。
4.2 预警模块:声音、弹窗还是外设
预警触发后做什么,取决于使用场景。桌面演示用winsound.Beep(Windows)或playsound播报警音最简单;要做得像样一点,可以用tkinter弹一个置顶窗口,或者用pygame.mixer循环播放提示音直到驾驶员确认。如果标题里提到"预警系统设计",通常还需要一个状态记录,把每次预警的时间戳写进日志文件,方便事后分析。
import time import csv def trigger_alarm(reason): # 声音预警:Windows 用 winsound,跨平台用 pygame try: import winsound winsound.Beep(1000, 500) except ImportError: print("ALARM:", reason) # 记录日志 with open("fatigue_log.csv", "a", newline="") as f: writer = csv.writer(f) writer.writerow([time.strftime("%Y-%m-%d %H:%M:%S"), reason])参数说明:winsound.Beep(1000, 500)是 1000Hz 响 500ms,频率太高刺耳、太低听不见,1000Hz 左右比较合适。日志用 CSV 追加写,字段是时间和触发原因,后续可以用 pandas 读出来做统计。注意预警要有冷却时间,比如触发后 10 秒内不重复报警,否则连续帧会刷屏。
4.3 把检测结果可视化出来
毕业设计答辩时,光有报警声不够,得有可视化。我一般会在画面上叠加三样东西:人脸框、眼睛和嘴巴的关键点连线、右上角的 EAR/MAR 实时曲线。曲线用cv2.line在画布上画,维护一个长度 100 的 EAR 历史队列,每帧把新值映射成 y 坐标。这样评委一眼就能看到 EAR 在闭眼时掉下去、MAR 在打哈欠时冲上去,比干讲公式有说服力。可视化代码不复杂,但要注意画布尺寸和坐标映射,别让曲线画出屏幕。
5. 避坑与排查:那些让我熬夜的细节
5.1 摄像头读不到帧或帧率骤降
现象:cap.read()返回 False,或者画面卡成幻灯片。原因通常是摄像头被其他程序占用,或者分辨率设太高导致 CPU 解码跟不上。解决:先确认没有其他软件开着摄像头;把cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)和高度设成 480,别用默认的 1080p;如果还是慢,把 dlib 检测器的上采样参数从 1 改回 0。另外 Linux 下要确认当前用户在 video 组里,否则权限不够。
5.2 EAR 阈值换个人就失效
现象:自己电脑上跑得好好的,换同学来测,睁眼 EAR 只有 0.18,一直误报闭眼。原因是个体眼型差异,双眼皮、单眼皮、眼睛大小都会影响 EAR 绝对值。解决:加一个 5 秒的标定环节,让用户正常睁眼几秒,程序自动统计 EAR 均值和标准差,动态生成阈值。这个改动不大,但能让系统从"只能演示"变成"能给别人用"。
5.3 戴眼镜反光导致关键点漂移
现象:戴眼镜的测试者,镜片反光时关键点会跳到镜框上,EAR 突然异常。原因是 dlib 的关键点模型对高光区域敏感。解决:预处理阶段加一步直方图均衡化(cv2.equalizeHist)压一下高光;如果还不行,在检测区域做一次高斯模糊再送进 predictor。更彻底的办法是换用对眼镜更鲁棒的模型,但毕业设计场景下,均衡化 + 模糊基本够用。
5.4 打哈欠和说话分不清
现象:测试者正常说话,MAR 频繁超过阈值,误报哈欠。原因是说话时嘴巴也在张合,单看 MAR 峰值区分不了。解决:哈欠的持续时间比说话的单次张口长,把判定条件从"MAR 超阈值"改成"MAR 连续超阈值超过 1.5 秒",说话时张口通常不到 1 秒,这样能过滤掉大部分误报。另外可以叠加 EAR 条件,真打哈欠时眼睛往往半闭,说话时眼睛是睁着的。
5.5 预警延迟太大或太灵敏
现象:要么闭眼好几秒才报警,要么眨个眼就响。原因是滑动窗口长度和连续帧阈值没配合好。解决:把窗口长度、平滑系数、连续帧数当成一组参数联合调。经验值是窗口 5 帧、平滑系数 0.3、闭眼连续 30 帧(2 秒)报警,先按这个跑,再根据实测微调。调参时录一段包含正常眨眼、闭眼、打哈欠的视频,离线跑一遍看误报和漏报,比对着摄像头反复试效率高得多。
6. 让这套系统更耐用的两个进阶技巧
第一个技巧是模型量化提速。dlib 的 68 点模型在 CPU 上跑,单帧约 30~50ms,如果要做多路摄像头或者嵌入式部署,这个速度不够。可以把 dlib 的模型转成 ONNX,再用 onnxruntime 推理,实测能快 2~3 倍。转换流程是先用 dlib 的shape_predictor导出,再用onnxruntime加载,输入输出对齐后替换掉原来的 predictor 调用。这一步对毕业设计来说是加分项,能体现工程优化意识。
第二个技巧是用轻量 CNN 做二次确认。EAR/MAR 是几何指标,遇到极端光照或遮挡会失效。可以在几何判定触发预警后,再截取眼部区域送进一个小型 CNN 做"睁/闭"二分类,两个结果都指向疲劳才最终报警。这个小 CNN 不用自己训,用公开的眼部状态数据集微调一个 MobileNet 就行,推理耗时增加不多,但误报率能明显下降。
验证方法上,我习惯用离线视频回放代替实时测试。录一段 5 分钟的视频,包含清醒、眨眼、闭眼、打哈欠、说话各种状态,用脚本逐帧跑检测,把 EAR/MAR 曲线和判定结果画出来,人工核对每个预警是否合理。这样调参有依据,答辩时也能拿出量化结果,比空口说"效果不错"强。
最后说个我自己的习惯:每次改完阈值或窗口参数,一定把当次的参数组合和对应的误报/漏报数记在一个表格里,跑够五六组再选最优。疲劳检测这套东西,玄学的地方就在参数耦合,凭感觉调很容易绕圈。把参数和结果记下来,翻车了也有后悔药可吃。希望帮到你。
本文还有配套的精品资源,点击获取