简介:面向计算机相关专业正在做毕业设计的学生和需要项目实战练习的学习者,这份基于Python+OpenCV的疲劳驾驶检测项目包含完整源码与全部运行数据,可直接作为毕设、课程设计或期末大作业使用。项目核心由driverFatigue.py主脚本、68点人脸关键点检测模型shape_predictor_68_face_landmarks.dat、中文字体simsun.ttc和说明文档组成,共4个文件,压缩包约74.52MB,代码经过严格调试,下载即可运行。已有1174人学习下载,适合初到中级水平的计算机视觉学习者。通过该项目可以掌握OpenCV图像读取与处理、dlib人脸关键点定位、眼睛纵横比EAR计算、疲劳状态判定等关键环节,并了解真实驾驶场景下图像数据与模型配套使用的完整流程。压缩包内文件分类清晰、数据齐全,便于直接部署、运行验证以及按需修改算法逻辑,是快速完成一个可演示、可答辩的工程项目的实用素材。
1. 疲劳驾驶检测毕业设计:Python + OpenCV 到底能做什么
连续闭眼三秒触发报警,这个动作背后不是玄学,而是一条可复现的视觉链路:OpenCV 拿到摄像头图像,Dlib 定位人脸 68 个关键点,再用眼睛纵横比 EAR 判断闭眼状态,连续帧累加触发疲劳提醒。这套基于 Python + OpenCV 的疲劳驾驶检测项目源码,把毕业设计最常用的传统视觉方案完整串了起来,不依赖 GPU,普通笔记本加一个摄像头就能当场演示。它适合两类人:一类是毕业设计想找个现成工程改改的同学,另一类是想快速上手传统视觉目标检测的从业者。接下来我按项目结构、核心代码、参数调优和踩坑记录逐层拆开,每一步都给出可以直接复制的判断依据。
2. 项目结构与选型:Dlib 关键点为什么是这套源码的骨架
2.1 目录结构:数据、模型、检测三层各管什么
拿到压缩包先别急着跑,把目录捋一遍再动手。这类毕设工程最常见的排布是三层结构,模型和数据分开,检测逻辑单独成模块:
fatigue_detection/ ├── data/ │ ├── eye_closed/ # 闭眼样本(自拍或视频截帧) │ ├── eye_open/ # 睁眼样本 │ └── demo_video.mp4 # 演示测试视频 ├── models/ │ ├── haarcascade_frontalface_default.xml │ └── shape_predictor_68_face_landmarks.dat ├── fatigue/ │ ├── face_detector.py # 人脸检测封装 │ ├── eye_ear.py # EAR 计算 │ ├── fatigue_judge.py # 连续帧判定 │ └── main.py # 主程序入口 ├── requirements.txt └── README.mddata 目录里的“全部数据”不要理解成深度学习训练集。我拆过不少同类毕设包,里面通常是作者用摄像头自拍的睁眼、闭眼、打哈欠照片,加上一段短视频,用途是验证阈值和做演示。models 目录里两个模型是核心,一个负责框出人脸,另一个负责定位 68 个关键点,后者决定整个方案的精度上限。fatigue 包里的代码按功能拆分,main.py 负责把摄像头、视频文件和界面显示串起来。
这里有个判断依据:如果目录里只有上述文件,没有训练脚本,说明这套方案是“人工规则 + 统计阈值”,不是“训练模型”。你在答辩时要讲清楚这一点,否则老师问“你的模型怎么训练的”你会当场卡壳。检测性能由 OpenCV 的 Haar 级联和 Dlib 的 HOG 关键点共同决定,两者缺一不可。
2.2 人脸检测选型:为什么用 Haar,而不是深度学习检测器
很多新手拿到这类项目,第一个疑问是“现在为什么还有人用 Haar 级联”。原因很务实:毕业设计要的是稳定、可解释、低消耗。Haar 级联的优点是毫秒级检测,缺点是对侧脸、光照变化敏感;Dlib 的 HOG 人脸检测精度高一些,但速度慢一些。这套项目常见做法是用 OpenCV 的 Haar 做粗定位,缩小搜索区域,再用 Dlib 做关键点精定位,两者配合,把计算量压到普通笔记本实时运行。
人脸检测部分的参数集中在 detectMultiScale 上,这些参数就是可调空间,我一般会按场景调三处:
| 参数 | 典型值 | 调大/调小的效果 |
|---|---|---|
| scaleFactor | 1.1 | 调小更精确但更慢,调大更快但容易漏脸 |
| minNeighbors | 5 | 调大减少误检,但真脸也可能被滤掉 |
| minSize | (80, 80) | 调大可以避开远处小脸,减少抖动 |
为什么要 minSize 设成 80?因为驾驶场景里人脸占了画面较大面积,较小的候选框基本都是误检。如果你在教室演示,摄像头离人远,脸很小,这个值要往下调。如果你这个包里只有 .xml 没有 .dat,说明作者用的是纯 OpenCV 方案,眼睛关键点会退化成眼睛级联检测,精度差一截,但 EAR 这套判定流程仍然可以参考——只是要提前在答辩时说明方案差异。
2.3 关键点与 EAR:把“睁眼闭眼”变成数字
人脸框出来只能知道“这里有个人”,不知道眼睛状态,这时候 Dlib 的 68 点模型上场。它是通过 HOG 特征和回归树级联训练的,输出 68 个点坐标,索引 36 到 41 是左眼,42 到 47 是右眼。EAR(Eye Aspect Ratio)是 Soukupova 和 Cech 在 2016 年提出的算法,公式如下:
EAR = (|p2 - p6| + |p3 - p5|) / (2 * |p1 - p4|)
其中 p1 到 p6 是环绕眼睛的六个关键点。分母是一条水平方向的距离,分子是两条垂直方向距离之和,闭眼时垂直距离变小,EAR 会从正常状态的 0.3 左右快速掉到 0.15 甚至 0.1。用比例而不是绝对像素距离,好处是对人脸远近、摄像头分辨率不敏感,这是这套方案能跨设备通用的原因。
打哈欠检测用同样的思路,嘴巴区域的 MAR(Mouth Aspect Ratio),阈值通常取 0.5 以上且持续多帧。很多毕设项目只做眨眼判断不做打哈欠,如果这套源码带打哈欠模块,答辩时的演示丰富度会高很多,后面调参时可以把 MAR 和 EAR 做并联判断,降低误报率。
2.4 三层衔接的逻辑与调试顺序
这套源码的调用链路很清晰:帧图像 → Haar 人脸框 → Dlib 关键点 → EAR → 连续帧判定 → 报警。调试时要从下往上逐层验证,每层的输出都用可视化的方式确认。常见做法是先显示人脸框,确认 Haar 没有漏检;再把 68 个点画出来,确认关键点没有漂移;最后打印 EAR 数值,确认眼睛状态能被量化表达。如果直接跳到最终报警,一个环节出错你很难定位是哪层的问题。我在第一次跑这套代码时就吃过这个亏,看起来是报警没触发,实际上是前面的人脸框压根就没画出来。
3. 跑通工程与核心代码:环境、人脸检测、EAR 计算和判定逻辑
3.1 环境搭建的版本锁:Python 3.8、OpenCV 4.x、Dlib 19.x
这是整个项目里最容易翻车的部分,大部分同学卡在 Dlib 编译而不是检测逻辑。我建议直接用 conda 建独立环境,把版本锁住:
conda create -n fatigue python=3.8 conda activate fatigue pip install opencv-python==4.5.5.64 pip install scipy pip install dlib==19.22.0上面这组版本是常见的稳定组合。dlib 在 Windows 上 pip 装经常需要本地有 C++ 编译环境,原因是它要编译 C++ 扩展;macOS 上通常装起来顺利,Linux 缺 libboost 则报 boost 相关错误。如果卡住,常见做法是用和 Python 版本匹配的预编译 wheel 文件安装。版本不要乱追最新,OpenCV 4.6 之后函数接口变化会影响部分老代码,Dlib 19.24 在部分环境下有段错误概率,这不是玄学,是社区反馈里常见的问题。requirements.txt 里我建议只写死这几个核心库,不要把 conda 的环境列表导进去,否则换机器复现时一堆无关包报错会干扰判断。
3.2 人脸检测模块:Haar 级联加载和参数含义
先把人脸检测单独拆出来,这一步输入是灰度图,输出是人脸矩形框列表:
import cv2 face_cascade = cv2.CascadeClassifier("models/haarcascade_frontalface_default.xml") def detect_faces(gray_frame): # 只有在灰度图上检测,Haar 特征本质是相邻区域的灰度差 faces = face_cascade.detectMultiScale( gray_frame, scaleFactor=1.1, minNeighbors=5, minSize=(80, 80) ) return facesdetectMultiScale 的三个参数直接决定人脸框稳不稳。scaleFactor=1.1 表示每次缩放窗口为原来的 90%,迭代搜索人脸;minNeighbors=5 要求每个候选框周围至少有五个框确认,误检率低;minSize 设置最小人脸尺寸。这里我把模型路径写死了,实际工程我会先判断文件是否存在再加载,否则路径错会直接抛 cv2.error。
注意模型文件路径是相对路径,必须在项目根目录下运行 main.py,或者改成绝对路径。中文路径是另一个隐藏杀手,Windows 下模型文件如果放在带中文的目录里,CascadeClassifier 加载会静默失败,返回空对象,后面调用 detectMultiScale 直接报错。这些坑后面第 4 章集中写。
3.3 EAR 计算模块:68 关键点怎么抽眼睛坐标
Dlib 拿到人脸框之后,用 shape_predictor 输出 68 个关键点。闭眼判断的关键是取对索引。左眼索引 36-41,右眼索引 42-47,这是 iBUG 300-W 数据集的通用编号方式:
import dlib from scipy.spatial import distance as dist detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("models/shape_predictor_68_face_landmarks.dat") def eye_aspect_ratio(eye_points): # 六个点按顺序从眼角到眼角环绕:p2-p6 和 p3-p5 是垂直距离,p1-p4 是水平距离 vertical_1 = dist.euclidean(eye_points[1], eye_points[5]) vertical_2 = dist.euclidean(eye_points[2], eye_points[4]) horizontal = dist.euclidean(eye_points[0], eye_points[3]) return (vertical_1 + vertical_2) / (2.0 * horizontal) def get_avg_ear(shape): left_eye = [(shape.part(i).x, shape.part(i).y) for i in range(36, 42)] right_eye = [(shape.part(i).x, shape.part(i).y) for i in range(42, 48)] left_ear = eye_aspect_ratio(left_eye) right_ear = eye_aspect_ratio(right_eye) return (left_ear + right_ear) / 2.0这里的顺序不能乱。Dlib 返回的 36 到 41 是按人眼轮廓顺时针排列的,眼头和眼尾的位置和 EAR 公式里的 p1、p4 是呼应关系。如果你把索引改错,EAR 曲线会出现乱跳的现象。左眼右眼取平均值是为了减少单眼因遮挡造成的误判,实际要是有一只眼睛被手遮住,平均值也会掉,所以更稳的做法是取两只眼睛 EAR 的较小值,这个可以在答辩时作为优化点说出来。
3.4 疲劳判定主循环:连续帧计数比单帧判断可靠得多
单帧低于阈值不能报警,人正常眨眼也会低于阈值。疲劳判定需要“连续帧”概念:
EYE_AR_THRESH = 0.22 CLOSED_SECONDS = 3 FPS = 30 closed_frames = 0 alarm = False # 每帧处理的核心逻辑,循环内部调用 def process_ear(avg_ear): global closed_frames, alarm if avg_ear < EYE_AR_THRESH: closed_frames += 1 else: closed_frames = 0 alarm = False return alarm if closed_frames >= CLOSED_SECONDS * FPS: alarm = True return alarm这里我用的是帧计数法而不是时间戳法。用 cv2.getTickCount 算时间在人脸检测耗时波动时会有误差,而帧数是稳定的。FPS 如果是摄像头实时输入,我一般取 30;如果是视频文件,要先用 cv2.VideoCapture 的 CAP_PROP_FPS 读取真实帧率,否则“3 秒”就不是 3 秒。判断条件闭眼满 90 帧(3 秒 * 30fps)才触发报警,这个数字和 EYE_AR_THRESH 一样,都是后面要标定的。
主循环里还有一个容易忽略的点:每帧结束后要用 cv2.waitKey(1) 释放事件缓存,否则画面窗口会无响应;如果检测到键盘按 q,就释放摄像头并销毁窗口。现场演示时窗口卡死多半是 waitKey 参数用成了 0(阻塞等待按键),导致视频流停滞。完整循环我一般写成:
while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detect_faces(gray) if len(faces) > 0: # 多人脸场景取面积最大的框,避免误检小框干扰 x, y, w, h = max(faces, key=lambda f: f[2] * f[3]) dlib_rect = dlib.rectangle(x, y, x + w, y + h) shape = predictor(gray, dlib_rect) avg_ear = get_avg_ear(shape) alarm = process_ear(avg_ear) color = (0, 0, 255) if alarm else (0, 255, 0) cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2) cv2.imshow("Fatigue Detection", frame) if cv2.waitKey(1) & 0xFF == ord("q"): breakmax(faces, key=lambda f: f[2] * f[3]) 是按宽高乘积取最大人脸,这个细节能挡住后排误检的小脸框。颜色切换也很直观,绿色是正常,红色是疲劳报警,答辩演示时视觉冲击力足够。
4. 避坑:模型路径、阈值误报、数据水分,五个真实翻车现场
4.1 五个高频翻车现场
这一节写的都是血泪经验。我整理这套工程中新手出现最多的 5 个问题,全部按现象、原因、解决顺序列出来:
问题 1:cv2.error: OpenCV(4.4.0) ... detectMultiScale 报错
- 现象:程序一运行到人脸检测就崩溃,报错信息指向 detectMultiScale,但代码看起来没问题。
- 原因:CascadeClassifier 加载模型失败,最常见是相对路径错误——在 IDE 里直接运行,工作目录不在工程根目录;其次是模型文件根本没被解压出来,或者文件名大小写不对。
- 解决:先判断模型对象是否为空,用 os.path.abspath 打印加载路径,确认文件真实存在;模型放在 models 目录,统一用绝对路径加载。
问题 2:摄像头打开黑屏或一直读不到帧
- 现象:cv2.VideoCapture(0) 返回空帧,画面黑屏但程序不退出。
- 原因:笔记本摄像头被其他软件占用,或虚拟机环境下摄像头索引不是 0,Windows 隐私设置也会屏蔽摄像头访问。
- 解决:换索引试试(1、2),先单独写两行代码验证摄像头,别直接跑整套逻辑。
问题 3:Dlib 加载模型时初始化失败或直接段错误
- 现象:shape_predictor 初始化卡住,甚至整个进程直接退出。
- 原因:dlib 版本和 Python 版本不匹配,或者模型文件下载中断损坏。dlib 19.22 在 Python 3.8 下是稳定组合,在 Python 3.9 上偶发异常。
- 解决:在 conda 环境里统一用 Python 3.8 + dlib 19.22,重新解压或重新下载模型文件,验证文件大小和源码包说明是否一致。
问题 4:闭眼判定不准,眨一下眼就报警
- 现象:人正常眨眼过程中 EAR 也会瞬时低于阈值,系统误报疲劳。
- 原因:阈值设得太高,或者连续帧数设置太小。人类正常眨眼持续约 100-150ms,30fps 下就是 3-4 帧,疲劳闭眼通常超过 400ms。
- 解决:把 EAR 阈值调到 0.18-0.20,连续帧数提高到 10 帧以上;更稳的办法是统计正常状态 1 分钟的 EAR 分布,用均值减去两个标准差作为阈值。
问题 5:检测不到侧脸或低头
- 现象:正脸正常,一头侧或低头,人脸框消失,EAR 计算中断。
- 原因:Haar 正面人脸级联是为正脸训练的,侧脸和低头时特征响应明显下降,这是该特征本身的局限。
- 解决:降级方案是用 Dlib 的 get_frontal_face_detector 先检测,它对姿态变化更鲁棒;进阶方案是两个检测器同时跑,取置信度高的结果。答辩前把侧脸场景测一遍,别在现场被打个措手不及。
4.2 数据集的水分:动辄“全部数据”的真实体量
压缩包名称里的“全部数据”四个字,拆包前要降低预期。这类毕设的数据集绝大多数是作者一个人的自拍样本:睁眼几十张、闭眼几十张、打哈欠十几张,外加一两段录屏。它不是公开的驾驶场景数据集,优点是演示时不会有太多意外情况,缺点是泛化能力没有说服力。
拿到数据后建议做三件事。第一,把睁眼、闭眼样本按文件夹分开,用脚本统计 EAR 的分布区间,看睁眼和闭眼会不会重叠,为阈值标定提供依据。第二,把演示视频里出现的人脸场景单独截出来,排除模糊帧和低照度帧,保证现场演示素材干净。第三,答辩时如实说明数据是自行采集的,不要吹成大型公开数据集,老师一旦深问就是穿帮。
4.3 判定参数是标定出来的,不是抄来的
网上很多博客直接给 EAR=0.25、连续 2 帧,这套参数在你自己的摄像头和光照下大概率误报成筛子。原因是 EAR 受内眼角形状、单双眼皮、戴眼镜反光影响,不同人的正常区间可以差 0.05 以上。我一般会在拿到任何疲劳检测源码后,先强制跑一遍正常 30 秒睁眼视频,记录 EAR 均值和标准差,然后设定阈值为均值减两倍标准差;再跑一段连续眨眼视频,验证眨眼不会被当成闭眼。这一步虽然多花十分钟,但至少能避免答辩现场翻车。
5. 进阶:把演示从本地视频切到摄像头,并校准 EAR 阈值
毕业设计演示时最怕的就是录好的视频看着正常,现场摄像头一开就翻车。核心原因在于视频文件的帧率和光照是固定的,而现场摄像头采集的人脸尺度、背景亮度、位置都会变。我有一个习惯,会把主程序的输入源从视频文件改成摄像头,并且把 EAR 值实时显示在画面上,一旦阈值不合适,当场就能看到。
# 本地视频改成摄像头,只改这一行输入源 cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30) # EAR 可视化,逐帧叠加到画面上 cv2.putText(frame, f"EAR: {avg_ear:.2f}", (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)切到摄像头有三个地方要同步改。帧率要从视频的固定值改成实际读取的 CAP_PROP_FPS;分辨率下调到 640x480,因为 Dlib 关键点在 720p 上单帧耗时约 20ms,每帧还要做人脸检测,分辨率太高会卡;minSize 也要调小,现场演示摄像头离人远,人脸经常只有一百像素左右,分辨率 480 时我建议把 minSize 设为 (60, 60)。
现场演示前,我固定调一遍校准流程。第一步,睁眼 30 秒,打印 EAR 均值作为 baseline。第二步,阈值取 baseline 减 0.05,保证正常状态不误报。第三步,连续眨眼 5 次,观察系统不会触发报警。第四步,闭眼 5 秒,确认报警触发且画面有明显的红色警示。这套流程也适用于录好的演示视频,只是把第一步输入换成视频文件。从那以后,我每次拿到这类疲劳驾驶源码,第一件事就是改输入源和打印 EAR,而不是先看结果界面——上来就调成视觉好看,往往后面埋着阈值错位的隐患。按这个顺序走一遍,基本不会在大屏演示时翻车,希望帮到你。
本文还有配套的精品资源,点击获取