基于OpenCV与Dlib的疲劳驾驶检测系统:从摄像头到报警的完整实现
简介这是一套面向高校计算机、人工智能及相关专业学生的毕业设计完整项目主题为基于Python与OpenCV的疲劳驾驶检测系统适合正在准备毕设、课程设计或想入门计算机视觉实战的开发者参考。压缩包共22个文件约93.53MB包含py源码、xml模型配置、dat数据文件、png与jpg图像素材、html页面及txt使用说明等覆盖算法实现、模型调用与界面展示等环节目录结构清晰便于按模块阅读与二次修改。该项目已获导师认可并高分通过代码经过实际运行测试功能正常后才整理上传可减少环境配置与调试踩坑成本。读者可从中获取完整的检测流程实现思路、OpenCV图像处理与疲劳特征判定的代码范例以及配套数据与说明文档适合作为毕设参考或视觉项目练手素材。目前已有1230人学习下载。1. 疲劳驾驶检测系统从摄像头到报警一条能跑通的链路长什么样凌晨两点跑高速眼皮开始打架方向盘轻微跑偏的那一瞬间如果车里有个东西能喊你一声很多事故就不会发生。疲劳驾驶检测系统要做的就是把这件事自动化用摄像头采集驾驶员面部画面靠 OpenCV 做人脸与眼睛定位再根据眼睛闭合时长、眨眼频率、打哈欠动作等指标判断疲劳程度最后触发声音或界面报警。它属于典型的计算机视觉落地项目也是计算机毕业设计里被选得最多、最容易讲清楚、又最容易翻车的题目之一。这套方案适合谁如果你正在做基于 Python 和 OpenCV 的毕业设计或者想找一个能写进简历、面试时能讲出细节的视觉项目它非常合适。硬件门槛低到一台带摄像头的笔记本就能跑软件栈就是 Python OpenCV 一个疲劳判定逻辑。但“能跑”和“能过答辩”之间隔着不少坑光照一变就丢人脸、阈值写死导致误报、帧率不够导致判定滞后。下面按“原理选型 → 环境搭建 → 核心检测 → 判定逻辑 → 避坑 → 调优”的顺序把这条链路拆开讲透。2. 疲劳检测的技术选型为什么是 OpenCV 而不是深度学习全家桶2.1 三类主流方案的能力边界对比做疲劳检测业内常见三条路线基于传统图像特征OpenCV Haar/Dlib、基于深度学习关键点MediaPipe、Dlib 68 点、基于生理信号方向盘握力、车道偏移。毕业设计场景下选型要同时考虑“实现难度、可解释性、答辩时能不能讲清楚”。方案核心依赖检测指标优点局限Haar 级联 模板匹配OpenCV 内置眼睛区域、嘴巴区域零额外模型、安装即用侧脸/戴眼镜易丢误检多Dlib 68 关键点dlib 预训练模型EAR、MAR 精确值精度高、指标可量化需下载模型文件编译 dlib 麻烦MediaPipe FaceMeshmediapipe468 点、EAR/MAR精度高、跨平台、速度快依赖较新部分老环境装不上我一般会推荐 Dlib 或 MediaPipe 做关键点因为疲劳判定的核心是 EAR眼睛纵横比和 MAR嘴巴纵横比这两个指标必须靠稳定关键点才能算准。Haar 级联虽然简单但它只能给你一个“眼睛大概在这个框里”算不出精确的闭合比例阈值稍微一动就误报。如果只是想让系统“动起来”Haar 够用想拿高分、想讲清楚 EAR 公式就上关键点方案。2.2 EAR 与 MAR两个决定成败的公式EAREye Aspect Ratio是疲劳检测里最经典的量化指标。它的思路是眼睛睁开时上下眼睑距离大闭合时这个距离趋近于零。用 6 个眼睛关键点算EAR (|p2-p6| |p3-p5|) / (2 * |p1-p4|)其中 p1、p4 是眼角左右点p2/p3 是上眼睑p5/p6 是下眼睑。正常睁眼 EAR 大约在 0.25~0.35闭眼会掉到 0.15 以下。MAR 同理用嘴巴上下点算打哈欠时 MAR 会明显升高。判定逻辑不是“某一帧 EAR 低就报警”而是“连续 N 帧 EAR 低于阈值”才判定为闭眼再累计闭眼时长。这个“连续帧”的设计就是抗抖动、抗单帧误检的关键。很多人第一次做直接if ear 0.2: 报警结果正常眨眼也报警这就是没做时序平滑的典型翻车。2.3 环境搭建Python、OpenCV、Dlib 的安装顺序环境是毕业设计第一道坎尤其是 dlib 在 Windows 上编译经常报错。推荐顺序先装 Python再装 numpy再装 opencv-python最后装 dlib 或 mediapipe。# 建议 Python 3.8~3.10太新版本 dlib 轮子可能没有 python --version # 先升级 pip避免装包时各种超时 python -m pip install --upgrade pip # 核心依赖opencv-python 提供 cv2 pip install numpy opencv-python # 关键点方案二选一 pip install dlib # 需要编译环境Windows 建议装 CMake 和 VS Build Tools pip install mediapipe # 更省事推荐新手先用这个参数说明opencv-python是主包opencv-contrib-python才带额外模块普通疲劳检测用前者就够。如果报ModuleNotFoundError: No module named opencv说明装到了错误的 Python 环境用python -m pip install opencv-python明确指定解释器。dlib 安装失败最常见原因是缺 CMake先pip install cmake再装 dlib。提示不要同时装 opencv-python 和 opencv-contrib-python两者会互相覆盖导致 cv2 行为异常。3. 用 OpenCV 跑通人脸与眼睛检测的最小闭环3.1 摄像头采集与灰度预处理第一步是把摄像头画面稳定读进来并转成灰度图。OpenCV 的人脸检测器对灰度图更友好彩色图反而增加计算量。import cv2 # 打开默认摄像头0 表示第一个摄像头 cap cv2.VideoCapture(0) # 设置分辨率太高会拖慢帧率640x480 是疲劳检测的甜点值 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 # 水平翻转让画面像照镜子符合直觉 frame cv2.flip(frame, 1) # 转灰度减少计算量 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) cv2.imshow(frame, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明cap.read()返回两个值ret表示是否读到帧必须判断否则摄像头被占用时会直接崩。cv2.flip的第二个参数 1 表示水平翻转0 是垂直翻转-1 是两者都翻。waitKey(1)里的 1 是毫秒值越小循环越快但太小可能来不及响应按键。分辨率设 640x480 是经验值1080p 会让关键点检测帧率掉一半疲劳判定需要实时性宁可低分辨率也不要卡顿。3.2 人脸检测Haar 与 DNN 两种加载方式OpenCV 自带 Haar 级联分类器文件在cv2.data.haarcascades目录下不用额外下载。import cv2 # 加载 OpenCV 自带的人脸分类器 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) # 加载眼睛分类器用于 Haar 方案下的眼睛定位 eye_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_eye.xml ) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # detectMultiScale 参数图像、缩放步长、最小邻居数、最小尺寸 faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(80, 80) ) for (x, y, w, h) in faces: cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) # 在人脸区域内找眼睛缩小搜索范围 roi_gray gray[y:y h, x:x w] eyes eye_cascade.detectMultiScale(roi_gray, 1.1, 10) for (ex, ey, ew, eh) in eyes: cv2.rectangle(frame, (x ex, y ey), (x ex ew, y ey eh), (255, 0, 0), 2)参数说明scaleFactor1.1表示每次图像尺寸缩小 10%值越小检测越慢但越全1.1 是速度和召回的平衡点。minNeighbors5控制误检值越大越严格人脸检测常用 5眼睛检测因为区域小可以调到 10 减少误检。minSize防止把远处的小噪点当人脸。Haar 方案最大的问题是侧脸和戴眼镜时眼睛框会丢所以它更适合做“能演示”的版本真正要算 EAR 还得靠关键点。3.3 用 Dlib 68 点算 EAR 的完整函数Dlib 的 68 点模型里左眼是 36~41右眼是 42~47嘴巴是 48~67。EAR 计算只需要眼睛的 6 个点。import dlib import numpy as np from scipy.spatial import distance as dist # 加载 Dlib 人脸检测器和 68 点预测器 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) def eye_aspect_ratio(eye_points): # 计算上下眼睑两组垂直距离 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 公式 ear (A B) / (2.0 * C) return ear def get_eye_points(shape, start, end): # 把 dlib 关键点转成 numpy 坐标数组 points [] for i in range(start, end): points.append((shape.part(i).x, shape.part(i).y)) return np.array(points) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) for face in faces: shape predictor(gray, face) left_eye get_eye_points(shape, 36, 42) right_eye get_eye_points(shape, 42, 48) left_ear eye_aspect_ratio(left_eye) right_ear eye_aspect_ratio(right_eye) # 双眼平均减少单眼误差 ear (left_ear right_ear) / 2.0逻辑说明dist.euclidean算两点欧氏距离A、B 是上下眼睑的两组垂直距离C 是眼角水平距离。除以2.0 * C做归一化这样不同人脸大小、不同摄像头距离下 EAR 都可比。双眼取平均是抗遮挡的常用手段一只眼被头发挡住时另一只还能撑住。shape_predictor需要下载shape_predictor_68_face_landmarks.dat模型文件这个文件约 100MB必须放在脚本同目录或写绝对路径否则报RuntimeError: Unable to open shape_predictor_68_face_landmarks.dat。注意Dlib 的detector(gray, 0)第二个参数是上采样次数0 表示不放大1 会放大一倍提升小脸检测率但速度减半。疲劳检测人脸通常够大用 0 即可。4. 疲劳判定逻辑阈值、计数器与报警触发4.1 闭眼判定连续帧计数而不是单帧阈值单帧 EAR 低于阈值不能说明疲劳正常眨眼也会瞬间低于阈值。正确做法是维护一个连续闭眼帧计数器。EYE_AR_THRESH 0.25 # EAR 低于此值认为闭眼 EYE_AR_CONSEC_FRAMES 20 # 连续多少帧闭眼才判定疲劳 COUNTER 0 ALARM_ON False # 在每帧循环内 if ear EYE_AR_THRESH: COUNTER 1 if COUNTER EYE_AR_CONSEC_FRAMES: if not ALARM_ON: ALARM_ON True print(疲劳报警请停车休息) else: COUNTER 0 ALARM_ON False参数说明EYE_AR_THRESH是闭眼阈值0.25 是常用起点但必须根据你的摄像头和光照实测调整。EYE_AR_CONSEC_FRAMES是连续帧数20 帧在 30fps 下约 0.67 秒正常眨眼通常 0.1~0.4 秒所以 20 帧能过滤掉眨眼。如果摄像头只有 15fps20 帧就是 1.3 秒判定会偏迟钝这时要按实际帧率换算。报警触发后不要每帧都打印用ALARM_ON标志位控制否则日志会刷屏。4.2 打哈欠检测MAR 与持续时间的组合打哈欠的 MAR 会从正常 0.3 左右升到 0.6 以上并且持续时间比说话长。判定逻辑和闭眼类似但阈值更高、持续帧数更长。MOUTH_AR_THRESH 0.6 MOUTH_AR_CONSEC_FRAMES 15 MOUTH_COUNTER 0 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 mar # 在关键点循环内 mouth get_eye_points(shape, 48, 68) mar mouth_aspect_ratio(mouth) if mar MOUTH_AR_THRESH: MOUTH_COUNTER 1 if MOUTH_COUNTER MOUTH_AR_CONSEC_FRAMES: print(检测到打哈欠) else: MOUTH_COUNTER 0逻辑说明嘴巴关键点 48~67 共 20 个这里取 0、2、4、6、8、10 六个点算 MAR和 EAR 公式结构一致。MOUTH_AR_THRESH0.6是经验值说话时 MAR 也会升高所以持续帧数要设得比闭眼更长15 帧在 30fps 下约 0.5 秒能过滤掉正常说话。打哈欠和闭眼可以组合判定闭眼 打哈欠同时出现疲劳等级更高可以触发更强的报警。4.3 报警输出声音、界面与日志三选一毕业设计里报警方式不用太复杂能演示清楚就行。常见三种终端打印、界面文字、声音提示。import cv2 import winsound # Windows 专用Linux 用 os.system 或 pygame # 方式一界面文字 cv2.putText(frame, FATIGUE ALERT, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3) # 方式二Windows 蜂鸣 if ALARM_ON: winsound.Beep(1000, 500) # 频率 1000Hz持续 500ms # 方式三写日志方便答辩时展示数据 with open(fatigue_log.txt, a, encodingutf-8) as f: f.write(f报警时间: {time.strftime(%Y-%m-%d %H:%M:%S)}\n)参数说明cv2.putText的坐标是文字左下角字体用FONT_HERSHEY_SIMPLEX最通用颜色 BGR 顺序红色是(0,0,255)。winsound.Beep只在 Windows 有效跨平台建议用pygame.mixer播放音频文件。日志文件用追加模式a不要用w否则每次运行都会覆盖历史记录。答辩时能拿出一份带时间戳的报警日志比空口说“它能报警”有说服力得多。5. 疲劳检测的避坑清单五个让系统翻车的真实原因5.1 光照一变就丢人脸现象白天在窗边跑得好好的晚上开台灯就检测不到人脸或者眼睛框乱跳。原因Haar 和 Dlib 的检测器都依赖灰度对比度侧光、逆光、过暗都会让面部特征模糊。解决加一个简单的直方图均衡化gray cv2.equalizeHist(gray)能明显改善暗光下的对比度。如果还不行在摄像头旁边补一个小台灯让光源从正面打过来这是最便宜有效的办法。5.2 阈值写死导致误报或漏报现象换个人、换个摄像头原来的 EAR 阈值就不准了要么一直报警要么闭眼也不报。原因EAR 绝对值受脸型、摄像头焦距、分辨率影响没有万能阈值。解决加一个校准阶段程序启动后前 3 秒采集用户正常睁眼的 EAR 平均值把阈值设为该平均值的 70%。这样每个人都能自适应答辩演示时也更稳。5.3 帧率不够导致判定滞后现象明明闭眼好几秒了系统才报警或者报警一闪就停。原因摄像头实际帧率低于预期EYE_AR_CONSEC_FRAMES按 30fps 设的实际只有 15fps判定时间翻倍。解决在循环里用time.time()算实际 FPS把连续帧数改成按时间判定比如“闭眼持续 0.8 秒”而不是“连续 20 帧”。时间判定比帧数判定更稳不受帧率波动影响。5.4 dlib 模型文件路径报错现象RuntimeError: Unable to open shape_predictor_68_face_landmarks.dat。原因模型文件没下载或者路径写的是相对路径但脚本工作目录不对。解决下载模型后放在脚本同目录用os.path.join(os.path.dirname(__file__), shape_predictor_68_face_landmarks.dat)拼绝对路径。不要用中文路径OpenCV 和 dlib 对中文路径支持都不好。5.5 摄像头被其他程序占用现象cap.read()一直返回 False画面出不来。原因摄像头被微信、腾讯会议、浏览器等程序占用同一时间只能一个程序访问。解决关掉所有可能用摄像头的软件或者把cv2.VideoCapture(0)的 0 改成 1 试试第二个摄像头。如果是笔记本自带摄像头检查有没有物理遮挡开关。6. 让疲劳检测更稳的三个进阶技巧6.1 用滑动窗口平滑 EAR 曲线单帧 EAR 有噪声直接拿来做判定会抖。滑动窗口取最近 N 帧的平均值曲线会平滑很多。from collections import deque # 保存最近 10 帧的 EAR ear_history deque(maxlen10) ear_history.append(ear) smooth_ear sum(ear_history) / len(ear_history) # 用平滑后的值做判定 if smooth_ear EYE_AR_THRESH: COUNTER 1deque的maxlen10表示窗口固定 10 帧超出自动丢弃最老的。窗口太小平滑效果弱太大判定滞后10 帧在 30fps 下约 0.33 秒是滞后和稳定的平衡点。这个技巧在答辩时很加分因为它体现了你对时序信号的理解而不是只会调库。6.2 分区域检测降低误检Haar 眼睛检测经常把眉毛、眼镜框误判成眼睛。一个实用技巧是只在人脸框的上半部分找眼睛因为眼睛一定在脸的上半区。# 人脸框上半部分作为眼睛搜索区域 roi_gray gray[y:y h // 2, x:x w] eyes eye_cascade.detectMultiScale(roi_gray, 1.1, 10)这个改动能把误检率降一大截因为下半张脸鼻子、嘴巴被排除了。如果用人脸关键点方案这个技巧用不上因为关键点本身就带语义不会把嘴巴当眼睛。6.3 用 EAR 基线做自适应阈值前面提过校准这里给一个更工程化的实现启动后前 90 帧约 3 秒只采集不判定算出一个基线 EAR。CALIB_FRAMES 90 calib_data [] if len(calib_data) CALIB_FRAMES: calib_data.append(ear) cv2.putText(frame, Calibrating..., (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2) else: if len(calib_data) CALIB_FRAMES: baseline sum(calib_data) / len(calib_data) EYE_AR_THRESH baseline * 0.7 # 基线 70% 作为闭眼阈值 calib_data.append(0) # 防止重复计算 # 正常判定逻辑baseline * 0.7这个系数是经验值因为闭眼时 EAR 大约降到睁眼的 60%~70%。不同人种、不同眼型会有差异但 0.7 是个稳的起点。校准阶段在界面上显示提示文字用户知道要睁眼配合体验也好。这个自适应逻辑写进毕业设计论文比固定阈值有说服力得多。我自己的习惯是任何视觉项目先把摄像头采集和显示跑通再加检测再加判定最后加报警。每一步都单独验证不要一次性写完再调否则出问题不知道是哪一层。疲劳检测尤其如此EAR 算错、阈值设错、帧率不够现象都是“它不报警”但原因完全不同。希望帮到你。本文还有配套的精品资源点击获取