简介:这是一套面向高校计算机、人工智能及相关专业学生的疲劳驾驶检测系统毕业设计完整源码包,基于Python与OpenCV实现,适合正在准备毕业设计或课程项目的学习者参考与二次开发。资源包共包含22个文件,整体约93.53MB,涵盖py主程序与检测脚本、xml级联分类器数据、dat模型文件、png与jpg图像素材、html说明页面及txt使用说明等,从算法实现到界面展示均有涉及,目录结构清晰,便于按模块查阅。目前已有1230人学习下载,说明其在实际教学场景中具备一定参考价值。该资源已获导师认可并高分通过,代码经过测试可正常运行,读者可据此理解疲劳检测的核心流程,包括人脸与眼部区域定位、特征提取及疲劳状态判定,并可直接运行验证效果,为毕业设计答辩与后续功能扩展提供扎实基础。
1. 从一份 zip 说起:疲劳驾驶检测系统到底在检测什么
拿到「基于python和opencv的疲劳驾驶检测系统源码+全部数据(毕业设计).zip」这个标题,多数人第一反应是去翻压缩包里有什么,但真正决定这套系统能不能跑起来、能不能过答辩的,是它背后那条检测链路。疲劳驾驶检测的本质不是「识别一张脸」,而是从连续视频帧里提取眼睛和嘴巴的状态,再用时间维度上的闭眼、打哈欠频率去判断一个人是不是困了。OpenCV 在这里承担的是图像预处理、人脸检测、关键点定位和可视化,Python 负责把这些环节串成一条可调试的流水线。
这套东西适合谁?一是做计算机毕业设计、需要一套能演示、能改、能写论文的学生;二是刚入门 OpenCV 图像处理项目、想找一个完整闭环练手的开发者。它解决的核心问题是:把「疲劳」这个模糊概念,量化成 EAR(眼睛纵横比)和 MAR(嘴巴纵横比)两个可计算的指标,再用阈值和帧计数做判定。理解了这条链路,源码就不再是黑匣子,参数也不再是玄学。
2. 疲劳判定的技术底座:EAR、MAR 与帧计数怎么配合
2.1 为什么不用「闭眼分类」而用纵横比
很多人第一版会想训练一个 CNN 做睁眼/闭眼二分类,但对毕业设计来说这是过度设计。原因有三:第一,训练数据要标注,工作量翻倍;第二,分类模型对光照、眼镜、侧脸敏感,泛化差;第三,答辩时讲不清网络结构反而扣分。EAR 的思路完全不同,它不判断「这是不是眼睛」,而是用眼睛周围 6 个关键点的几何关系算一个比值。
EAR 的定义是:眼睛垂直方向两组点距离之和,除以水平方向点距离的两倍。眼睛睁开时,垂直距离大,EAR 在 0.25 到 0.35 之间;闭眼时垂直距离趋近于零,EAR 掉到 0.15 以下。这个方法的妙处在于它是尺度无关的——人脸离镜头远近,分子分母同步变化,比值稳定。MAR 同理,用嘴巴上下唇关键点算张开程度,打哈欠时 MAR 会明显高于正常说话。
提示:EAR 阈值不是固定值,不同人眼型差异很大,源码里如果写死 0.2,换个人就可能误报。后面第 5 章会讲自适应阈值。
2.2 关键点从哪来:dlib 与 OpenCV 的分工
OpenCV 自带的人脸检测器(Haar 或 DNN)只能给出人脸框,给不出眼睛、嘴巴的精确坐标。所以这类项目通常引入 dlib 的 68 点人脸关键点模型,其中第 36-41 点是左眼,42-47 点是右眼,48-67 点是嘴巴。OpenCV 负责读帧、缩放、画框、显示,dlib 负责关键点回归,两者配合是这套源码最常见的架构。
如果你在环境里装 dlib 遇到编译报错,一个常见替代是 MediaPipe,它的人脸网格模型同样能给出眼睛和嘴巴坐标,而且 pip 安装不需要编译。但要注意,MediaPipe 的点位编号和 dlib 不同,源码里的索引要跟着改,否则算出来的 EAR 是错的。这是换库时最容易翻车的地方。
2.3 单帧判定为什么不可靠:引入帧计数
只靠一帧 EAR 低就报警,结果就是眨眼瞬间疯狂误报。人正常眨眼大约 100 到 400 毫秒,如果视频是 30 帧每秒,一次眨眼只占 3 到 12 帧。所以判定逻辑必须是:连续 N 帧 EAR 低于阈值,才认为是一次闭眼事件;连续闭眼累计超过一定时长(比如 2 秒),才判定为疲劳。
这就引出了两个计数器:一个是连续闭眼帧数eye_counter,一个是打哈欠次数yawn_count。源码里通常还会加一个总疲劳分数,闭眼加分、打哈欠加分,分数超过阈值就触发警报。这套逻辑不复杂,但参数之间的配合决定了系统的可用性,下一章直接上代码。
3. 把源码跑起来:环境、依赖与最小可运行脚本
3.1 环境搭建:Python 版本与依赖安装
这套源码对 Python 版本不挑,3.8 到 3.11 都能跑,但要注意 dlib 在 3.12 上编译经常出问题,稳妥起见用 3.9 或 3.10。安装顺序有讲究,先装 numpy 再装 opencv,最后装 dlib,因为 dlib 编译时依赖 numpy 的头文件。
# 建议在虚拟环境里操作,避免污染全局 python -m venv venv # Windows 激活 venv\Scripts\activate # Linux / macOS 激活 source venv/bin/activate # 按顺序安装,numpy 必须先于 dlib pip install numpy==1.24.3 pip install opencv-python==4.8.1.78 pip install dlib==19.24.2 pip install scipy imutils这里固定版本不是强迫症,而是血泪经验:opencv-python 4.9 之后某些 API 有变动,dlib 19.24 是最后一个在 Windows 上预编译包比较全的版本。如果你遇到ModuleNotFoundError: No module named 'cv2',八成是装到了另一个 Python 环境里,用python -c "import sys; print(sys.executable)"确认解释器路径。
3.2 关键点模型文件放哪、怎么读
dlib 的 68 点模型文件shape_predictor_68_face_landmarks.dat大约 99MB,压缩包里一般会带。代码里读取时路径写错是最常见的翻车点,建议用相对路径加断言检查。
import os import dlib import cv2 import numpy as np # 模型路径,放在项目根目录的 models 文件夹下 PREDICTOR_PATH = os.path.join("models", "shape_predictor_68_face_landmarks.dat") # 启动时就检查,别等到运行中途才报错 if not os.path.exists(PREDICTOR_PATH): raise FileNotFoundError(f"关键点模型不存在: {PREDICTOR_PATH}") detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor(PREDICTOR_PATH)get_frontal_face_detector是 dlib 的 HOG 人脸检测器,速度比 CNN 版快,CPU 上就能实时。shape_predictor加载模型后,对每张人脸框返回 68 个坐标点。注意 detector 返回的是 dlib 的 rectangle 对象,不是 OpenCV 的坐标,画框时要转换。
3.3 EAR 与 MAR 的计算函数
把几何计算封装成函数,是整个源码里最值得单独拎出来讲的部分。下面这段可以直接抄。
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): # 用嘴巴上下唇中间点算张开程度 A = dist.euclidean(mouth_points[2], mouth_points[10]) B = dist.euclidean(mouth_points[4], mouth_points[8]) C = dist.euclidean(mouth_points[0], mouth_points[6]) mar = (A + B) / (2.0 * C) return mardist.euclidean算两点欧氏距离。EAR 公式里分子是两组垂直距离之和,分母是水平距离乘 2,这个 2 是为了归一化,让睁眼时 EAR 落在 0.3 附近,方便设阈值。MAR 用的是嘴巴 20 点模型里的索引,如果你用的是 68 点模型,嘴巴是 48-67,索引要相应平移。参数说明:eye_points必须是 6 个点且顺序正确,顺序错了 EAR 会算出离谱的值,这是新手最容易忽略的。
3.4 主循环:从读帧到报警的完整链路
把上面拼起来,就是一个最小可运行版本。下面这段代码去掉了界面美化,只保留判定逻辑,方便你理解主干。
EYE_AR_THRESH = 0.25 # 闭眼阈值 EYE_AR_CONSEC_FRAMES = 3 # 连续多少帧算一次闭眼 YAWN_THRESH = 0.6 # 打哈欠阈值 COUNTER = 0 # 连续闭眼帧计数 TOTAL_BLINKS = 0 # 闭眼事件总数 YAWN_COUNT = 0 # 打哈欠次数 FATIGUE_SCORE = 0 # 疲劳总分 cap = cv2.VideoCapture(0) # 0 是默认摄像头,也可换成视频文件路径 while True: ret, frame = cap.read() if not ret: break frame = cv2.resize(frame, (640, 480)) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 0) for face in faces: shape = predictor(gray, face) # 把 dlib 关键点转成 numpy 坐标 coords = np.array([(shape.part(i).x, shape.part(i).y) for i in range(68)]) left_eye = coords[36:42] right_eye = coords[42:48] mouth = coords[48:68] ear = (eye_aspect_ratio(left_eye) + eye_aspect_ratio(right_eye)) / 2.0 mar = mouth_aspect_ratio(mouth) if ear < EYE_AR_THRESH: COUNTER += 1 else: if COUNTER >= EYE_AR_CONSEC_FRAMES: TOTAL_BLINKS += 1 FATIGUE_SCORE += 1 COUNTER = 0 if mar > YAWN_THRESH: YAWN_COUNT += 1 FATIGUE_SCORE += 2 # 疲劳判定 if FATIGUE_SCORE > 20: cv2.putText(frame, "FATIGUE ALERT", (150, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3) cv2.imshow("Fatigue Detection", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()逻辑说明:每帧先检测人脸,再取关键点算 EAR 和 MAR。闭眼时累加COUNTER,睁眼时如果之前连续闭眼超过阈值帧数,就记一次眨眼并加疲劳分。打哈欠直接加 2 分。FATIGUE_SCORE超过 20 触发警报。参数怎么调:EYE_AR_THRESH从 0.25 开始试,戴眼镜的人可能要降到 0.2;EYE_AR_CONSEC_FRAMES设 3 是防止单帧噪声,设太大漏检;YAWN_THRESH0.6 是经验值,张嘴说话可能误触发,后面会讲怎么区分。
4. 避坑与排查:这套源码最容易翻车的五个地方
4.1 摄像头读不到帧或画面全黑
现象:cap.read()返回 False,或者窗口一片黑。原因通常是摄像头被其他程序占用,或者VideoCapture(0)的索引不对。解决:先换索引试 1 或 2,再检查是否有其他软件(会议、直播工具)占着摄像头。Linux 下还要确认当前用户有/dev/video0的读权限,用ls -l /dev/video*看。
4.2 EAR 值始终异常,闭眼睁眼没区别
现象:打印出来的 EAR 一直在 0.5 以上,或者一直低于 0.1。原因几乎都是关键点索引取错了,或者坐标顺序不对。解决:把coords[36:42]画到图上,用cv2.circle标出来,肉眼确认这 6 个点是不是正好在眼睛上。如果用的是 MediaPipe,索引完全不同,必须查它的官方点位图重新映射。
4.3 打哈欠误报:说话也被算成哈欠
现象:正常说话时YAWN_COUNT疯涨。原因是 MAR 只看嘴巴张开程度,说话时嘴也会张。解决:加一个持续时间条件,嘴巴张开必须连续超过 15 帧才算一次哈欠,说话的张合通常更快。另外可以把 MAR 阈值提到 0.65 以上,减少误触发。
4.4 帧率不稳导致计数失真
现象:同一段视频,有时报警有时不报。原因是EYE_AR_CONSEC_FRAMES是按帧数算的,但摄像头帧率会波动,30fps 和 15fps 下同样 3 帧代表的时间差一倍。解决:改用时间戳计算闭眼持续时长,用time.time()记录闭眼开始时刻,超过 0.4 秒才算一次闭眼事件,这样和帧率解耦。
4.5 打包成 exe 后模型文件找不到
现象:源码里跑得好好的,用 PyInstaller 打包后报模型文件不存在。原因是打包后工作目录变了,相对路径失效。解决:用sys._MEIPASS定位打包资源目录,或者把模型文件路径改成基于os.path.dirname(os.path.abspath(__file__))的绝对路径。这是毕业设计答辩现场演示时最尴尬的翻车,提前测一遍。
5. 让检测更稳:自适应阈值与多指标融合的进阶做法
5.1 用前 3 秒校准个人基线
固定阈值最大的问题是因人而异。一个简单有效的改进是:程序启动后前 3 秒不判定,只采集用户正常睁眼状态下的 EAR 均值,然后把这个均值的 70% 作为闭眼阈值。这样戴眼镜、眼型小的人也能适配。
calibration = [] CALIB_FRAMES = 90 # 约 3 秒 # 在主循环里,前 90 帧只采集不判定 if len(calibration) < CALIB_FRAMES: calibration.append(ear) cv2.putText(frame, "Calibrating...", (200, 240), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2) continue else: if EYE_AR_THRESH is None: baseline = np.mean(calibration) EYE_AR_THRESH = baseline * 0.7 print(f"校准完成,基线 EAR={baseline:.3f},阈值={EYE_AR_THRESH:.3f}")CALIB_FRAMES设 90 是按 30fps 算的 3 秒,如果摄像头帧率低就相应减少。baseline * 0.7这个系数是经验值,0.65 到 0.75 之间都可以试。校准期间要提示用户保持正常睁眼,别眨眼,否则基线偏低。
5.2 把眨眼频率和 PERCLOS 一起看
单看闭眼时长还不够,真正有说服力的是 PERCLOS(单位时间内闭眼时间占比)。它的定义是:统计窗口内闭眼帧数除以总帧数。正常驾驶 PERCLOS 低于 0.2,疲劳时超过 0.4。把 PERCLOS 和打哈欠次数一起作为判定依据,比单纯累加分数更符合学术规范,答辩时也更好讲。
| 指标 | 正常范围 | 疲劳预警 | 计算方式 |
|---|---|---|---|
| EAR | 0.25-0.35 | <0.2 持续 | 关键点几何比值 |
| MAR | 0.3-0.5 | >0.6 持续 | 嘴巴张开程度 |
| PERCLOS | <0.2 | >0.4 | 闭眼帧数/总帧数 |
| 眨眼频率 | 15-20 次/分 | <10 或 >30 | 单位时间闭眼事件 |
这张表可以直接放进论文的指标章节。注意 PERCLOS 的统计窗口一般取 30 秒或 60 秒,窗口太短波动大,太长反应迟钝。
5.3 一个我踩过的坑:别在循环里反复加载模型
早期版本我把dlib.shape_predictor写在了 while 循环里,结果帧率掉到 5fps,还以为是电脑不行。模型加载是磁盘 IO 加内存分配,必须放在循环外只做一次。同理,cv2.CascadeClassifier也不要每帧新建。这个习惯养成后,同样的硬件帧率能翻好几倍。
5.4 验证方法:用录制视频代替真人测试
调试时别一直对着摄像头,累且不可复现。用手机录一段自己正常、眨眼、打哈欠、低头四种状态的视频,存成 mp4,把VideoCapture(0)改成文件路径,逐段跑,打印 EAR 和 MAR 曲线。这样能快速定位阈值问题,也方便截图放进论文。我一般会录 30 秒一段,跑完看报警时间点对不对,比对着镜头反复试高效得多。
这套源码的价值不在于它多完美,而在于它是一条完整的、可拆解、可替换的链路。你可以把 dlib 换成 MediaPipe,把 EAR 换成 CNN,把单摄像头换成红外,但「提取指标、时间维度判定、阈值校准」这个骨架不会变。把校准和 PERCLOS 加上,答辩时就有了讲不完的细节。希望帮到你。
本文还有配套的精品资源,点击获取