ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

疲劳驾驶检测实战:Python+OpenCV+dlib实现EAR与PERCLOS判定

疲劳驾驶检测实战:Python+OpenCV+dlib实现EAR与PERCLOS判定 简介基于Python与OpenCV的疲劳驾驶检测项目适合计算机相关专业正在准备毕业设计的学生也适合需要项目实战练习的初级学习者用于课程设计、期末大作业。项目重点覆盖人脸关键点定位、眨眼/打哈欠检测等疲劳判定流程并提供一套可直接运行的完整实现方案。资源为zip压缩包共74.52MB内含4个文件Python程序源码、预训练人脸关键点模型.dat、中文字体.ttc及依赖说明文档.txt可支撑从环境配置到算法演示的全过程。其中.dat模型用于人脸对齐和特征提取.ttc用于可视化中文显示。该毕设项目经导师指导并认可评审分98分源码已完成本地编译与严格调试能够直接运行。项目代码与全部数据一并打包便于对照学习与二次开发。已有154人学习下载可帮助快速搭建实验环境、理解典型视觉技术栈适合作为计算机视觉方向练手与答辩展示的参考资料。1. 疲劳驾驶检测项目在做什么一个被低估的难点把疲劳驾驶检测当成“眼睛闭上就报警”是新手最容易翻车的地方。真实驾驶舱里光线忽明忽暗、司机戴墨镜或低头、摄像头装在仪表台上还会抖任何一个因素都能让看似简单的逻辑失效。基于 python opencv 的疲劳驾驶检测项目核心不是把模型堆得多高级而是用 dlib 的 68 点关键点算出眼睛纵横比EAR再用 PERCLOS 判定疲劳状态整条链路简单、可跑、能讲清楚。源码和全部数据一起给出意味着你不用从零收集素材重点放在调参、评估和写论文上。适合要把毕业设计做扎实、又想少踩环境坑的人前提是你得知道这套代码每行在干什么。2. 源码与核心原理EAR、PERCLOS 和数据文件三件事2.1 68 点关键点与 EAR 眨眼判据一段能直接跑的示例疲劳驾驶检测在视觉层面做的事可以压缩成一句话找到脸定位眼睛算眼睛张开的程度。dlib 在这里承担人脸检测和关键点定位它会在人脸上标出 68 个关键点左右眼各占 6 个。36 到 41 是左眼42 到 47 是右眼坐标顺序从眼角出发、沿眼睑绕一圈。有了这 6 个点就能算眼睛纵横比EAR它和摄像头距离、脸的大小基本无关只反映眼睛的相对形状。这也是为什么 EAR 能替代“数黑眼珠像素”这类脆弱方案的原因。import numpy as np def eye_aspect_ratio(eye): # eye 是按顺序排列的 6 个关键点坐标shape 为 (6, 2) p1, p2, p3, p4, p5, p6 eye # 纵向距离取两段求均值抗关键点抖动 vertical np.linalg.norm(p2 - p6) np.linalg.norm(p3 - p5) # 横向距离是眼角 p1 到 p4 的欧氏距离 horizontal np.linalg.norm(p1 - p4) # 分母乘 2 是纵向取均值的还原1e-6 防止除零 return vertical / (2.0 * horizontal 1e-6)这段代码的输入是 6 个点输出是一个无量纲数值。正常睁眼时 EAR 在 0.25 到 0.3 附近完全闭合时会掉到 0.1 以下所以阈值通常取 0.2 到 0.25 之间。注意这里 eye 的点的顺序必须和 dlib 输出的索引一致否则算出来的是左右眼角距离比上一个乱七八糟的线调参再久也没用。很多改版项目在这里把眼睛索引搞错导致后面所有统计都失效这是第一个要核对的地方。EAR 只是单帧的眼睛状态它不能直接回答“人困不困”因为它受个体差异影响很大。有人天生眼睛小睁眼 EAR 也只有 0.22有人戴眼镜镜片反光让关键点上下跳动。所以实用做法是用一个滑动队列对这 5 到 10 帧的 EAR 取平均先消除单帧噪声再交给疲劳判定逻辑。2.2 PERCLOS 疲劳判据闭眼时间占比才是关键指标单帧 EAR 只能告诉你这帧眼睛是不是闭上了但疲劳是一个时间维度上的状态。PERCLOSPercentage of Eye Closure统计的是一个时间窗口内眼睛闭合帧数占总帧数的比例。它比“连续几帧闭眼”更贴近驾驶疲劳的真实定义一个人开车时偶尔眨眼是正常生理反应真正危险的是在 30 秒内闭眼时间占比持续升高。# 假设 30fps 下30 秒一个统计窗口 WINDOW_FRAMES 900 EAR_THRESH 0.22 closed_frames 0 frame_count 0 while True: ear get_current_ear() # 来自上一节的平滑 EAR frame_count 1 if ear EAR_THRESH: closed_frames 1 if frame_count WINDOW_FRAMES: perclos closed_frames / frame_count if perclos 0.2: trigger_alarm() # 窗口结束清零重新统计 closed_frames 0 frame_count 0这里两个参数值得展开窗口长度和 PERCLOS 阈值。窗口太短比如 5 秒偶尔两次正常眨眼就会触发误报窗口太长比如 2 分钟疲劳报警来得太迟失去了预防意义。主流项目用 30 秒到 60 秒阈值从 0.2 起步现场实测后再往 0.3、0.4 调。PERCLOS 的原始定义还区分 P70、P80 两种标准P80 指眼睛闭合面积超过 80% 才算闭眼但用 EAR 实现时“闭眼”直接简化为 EAR 小于阈值因为关键点距离比无法直接换算成面积占比这是工业实现和学术定义之间的常见妥协论文里要主动写清楚。2.3 源码包里需要先认识的几个文件拿到源码后不要急着点运行先花十分钟把目录过一遍。典型的疲劳驾驶检测项目结构大致如下你的压缩包里可能名字不同但职责是匹配的文件/目录职责你大概率要改的地方main.py 或 run.py主循环读帧、检测、画框、统计改成自己的摄像头索引和报警逻辑utils.py 或 eye_utils.pyEAR 计算、PERCLOS 统计调阈值、改平滑窗口shape_predictor_68_face_landmarks.datdlib 的 68 点模型路径别写错别放中文目录data/ 目录图片帧、视频片段、标注 CSV论文实验的数据来源标注文件.csv / .txt每帧或每个片段的状态标签重新清洗时重点看这里“全部数据”在这个项目里通常分三类一是睁眼、闭眼、打哈欠的图片帧用来做分类或展示检测效果二是连续驾驶片段用于跑 PERCLOS 整体流程三是标注文件规定哪些帧算闭眼、哪些算正常。这里最容易踩的坑是标注和视频帧的命名对不上比如帧名是 frame_0001.jpgCSV 里却写着 frame_1.jpg肉眼看不出来脚本一跑就报错。拿到数据先写个脚本检查图片数量和标注行数是否一致这种血泪经验能省下你半天时间。源码里还有一个常见模块是“打哈欠检测”利用 68 点中的嘴巴轮廓点计算嘴巴纵横比MAR思路和 EAR 完全一样。很多高分毕业设计会把眨眼和打哈欠两个通道并行做然后取“闭眼 OR 打哈欠”作为疲劳报警条件逻辑简单但把论文的故事讲圆了你做的不是单指标判断而是多模态疲劳证据融合。3. 把项目跑起来环境配置与最小可运行命令3.1 安装 opencv 和 dlib版本与编译的注意点这个项目对环境版本比较敏感尤其是 Windows 上装 dlib 时经常现场翻车。先说结论用 Python 3.8 或 3.9opencv 装 4.2 及以上版本dlib 用 19.22 或 19.24避免最新 Python 版本带来的二进制包缺失问题。网上搜“安装opencv”找到的大多是老教程还在让人配 Anaconda 下的 python 2.7千万别照着搬。# 基础安装命令注意三个包分别装 pip install opencv-python4.5.5.64 pip install dlib19.22.0 pip install imutils # dlib 如果走源码编译需要先准备好 cmake 和 C 编译工具 # Windows 下要先装 Visual Studio Build Tools 的 C 工作负载装完先验证 import 是否正常。常见的是 modulenotfounderror: no module named opencv或者报 no module named cv2原因往往是 pip 装了 opencv-contrib-python 却又用 opencv-python 去覆盖或者虚拟环境没有激活。我的习惯是在项目根目录建一个 venv激活后再统一装依赖然后写一步验证脚本python -c import cv2, dlib; print(cv2.__version__); print(dlib.__version__)能同时打印出 opencv 和 dlib 版本号才说明环境这一步过关。dlib 在 Windows 上 pip 装失败时优先尝试 pypi 上对应的 wheel而不是一上来就编译源码。编译 dlib 需要 cmake 和 VS Build Tools整个过程十几分钟期间没有一个输出提示容易让人以为卡死了实际上它就是在硬扛。3.2 摄像头采集主循环最小可运行代码环境就绪后第一个目标不是跑通全部功能而是先看到摄像头画面里出现人脸框和关键点。主循环的骨架以下面这段为准它包含了打开设备、检测人脸、标关键点、显示画面四个必要环节import cv2 import dlib # 初始化检测器和关键点模型 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) # 摄像头默认索引是 0笔记本外接摄像头可能是 1 cap cv2.VideoCapture(0) if not cap.isOpened(): raise IOError(摄像头打不开检查设备索引或权限) while True: ret, frame cap.read() if not ret: print(读帧失败) break # dlib 检测输入是灰度图速度更快 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 第二个参数 1 表示对图像做一次上采样以提高小脸检出率 faces detector(gray, 1) for face in faces: shape predictor(gray, face) # 这里 shape 就是 68 个关键点后续可以转成 numpy 数组 # 左眼取 shape.part(36) 到 shape.part(41) # 右眼取 shape.part(42) 到 shape.part(47) cv2.imshow(Fatigue Detection, frame) # waitKey(1) 表示每帧等待 1 毫秒并按 q 退出 if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码是后面所有功能的地基。参数 detector(gray, 1) 里的上采样次数很关键值为 0 时速度最快但小脸容易漏检值为 1 时检出率提升但每帧耗时增加。驾驶场景下摄像头离人比较近用 0 或 1 都行如果是后排摄像头建议用 1。另外灰度转换这一步别省dlib 的 HOG 检测器不认彩色图直接传彩色图会报类型错误。运行起来后你先观察一件事人脸框是否稳定。如果人脸框在跳说明检测器在“丢脸”和“找回脸”之间反复这会直接导致关键点闪烁。解决方案是把上一帧的人脸矩形作为下一帧的初始位置传给 detector用 gain 参数做跟踪式检测但那是后话第一版先把单帧跑稳。3.3 shape_predictor 模型路径最常见的启动报错这个项目里新手遇到的第一道坎九成是 shape_predictor_68_face_landmarks.dat 加载失败。现象是程序一启动就抛出 RuntimeError提示 Unable to open shape_predictor_68_face_landmarks.dat。原因通常有三个路径写错、文件没下载完整、程序工作目录不在模型所在目录。# 推荐的做法用绝对路径拼模型位置避开当前工作目录问题 import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) MODEL_PATH os.path.join(BASE_DIR, models, shape_predictor_68_face_landmarks.dat) predictor dlib.shape_predictor(MODEL_PATH)这里用 os.path.dirname 取当前脚本所在目录再去 models 子目录找模型文件无论你用命令行还是 IDE 启动都不会因为工作目录不同而报错。另一个常见问题是模型文件从下载源拿到后是损坏的dlib 加载时并不校验完整性它在第一次 predict 时才崩溃表现是程序正常启动但一检测到人脸就报内存错误。判断方法很简单看文件大小是否合理以及能不能用 dlib 的示例代码加载后跑一次 predict。如果模型文件确实不完整我的建议是重新下载并保留原始压缩包别用下载工具的中断续传。文件放进项目后用上面这段绝对路径加载一次跑通后再继续往下调。这一步没跑通之前任何关于疲劳判据的讨论都没有意义因为程序根本到不了眼睛关键点那一层。4. 参数调优哪些值决定报警准不准4.1 EAR 阈值的取值边界与眨眼时长EAR 阈值是整套系统里最“玄学”的参数因为没有一个通用值能适配所有摄像头安装位置。同一个人的同一状态摄像头装在挡风玻璃后和装在方向盘前方EAR 数值可以差出 0.05。所以我把调参分成两步先确定阈值再确定闭眼帧数。# 滑动平滑消除关键点抖动带来的单帧尖刺 from collections import deque EAR_THRESH 0.22 CONSEC_FRAMES 3 # 连续闭眼 3 帧才算一次眨眼 ear_history deque(maxlen5) closed_count 0 def smooth_ear(raw_ear): ear_history.append(raw_ear) return sum(ear_history) / len(ear_history) # 主循环内 ear smooth_ear(get_raw_ear()) if ear EAR_THRESH: closed_count 1 else: if closed_count CONSEC_FRAMES: print(检测到一次眨眼) closed_count 0平滑窗口 maxlen5 意味着用最近 5 帧的平均值代表当前眼睛状态窗口越大越稳但响应越慢。闭眼帧数 CONSEC_FRAMES 起到“防抖”作用因为 EAR 偶尔一帧低于阈值可能是关键点跳变持续 3 帧才判定为闭眼。这两个参数配合关系是如果你想提高眨眼召回率优先降 CONSEC_FRAMES而不是降 EAR_THRESH因为后者会把半闭眼状态也卷入噪声。判断当前阈值合不合理有一个笨但有效的办法对着摄像头连续睁眼、闭眼各 10 次打印出睁眼时最低的 EAR 和闭眼时最高的 EAR阈值取两者的中间值。这个中间值通常落在 0.18 到 0.25 区间如果低于 0.18多半是光线太暗或摄像头离人太远先把安装位置调好再来调阈值。4.2 PERCLOS 窗口时长和疲劳阈值怎么定PERCLOS 的两个参数是窗口帧数和报警阈值它们共同决定了系统“反应快不快”和“误报多不多”。窗口帧数的计算公式是窗口秒数 × 摄像头帧率。30fps 下 30 秒窗口就是 900 帧60 秒窗口就是 1800 帧。我见过不少项目直接把窗口帧数写成 900没考虑实际帧率只有 15结果真实窗口长达 60 秒报警迟钝得离谱。参数建议初始值调参方向PERCLOS 窗口30 秒30fps 下 900 帧误报多就调大到 60 秒PERCLOS 阈值0.2太灵敏就调大到 0.30.4EAR 阈值0.22按实测中间值修正眨眼判定帧数3 帧检测不到眨眼就降到 2 帧调参的正确顺序是从下往上先定 EAR 阈值再定眨眼帧数最后调 PERCLOS 参数。如果一上来就收紧 PERCLOS 阈值你会发现系统对任何一点的 EAR 波动都极其敏感因为在 30 秒窗口里只要少数几帧闭眼就很容易超过 0.1 的小比例反而难以区分疲劳和正常眨眼。报警触发后还要设计一个“冷却时间”。假设系统判定疲劳后立即报警报警持续 5 秒那么这 5 秒内的帧不应该再计入下一个 PERCLOS 窗口否则连续报警会把你逼疯。常见做法是触发报警后把当前窗口清零、同时暂停统计 10 秒让系统回到一个干净状态。4.3 摄像头角度、光线与报警策略的配合参数调得再好摄像头安装角度不对也白搭。俯拍时人脸关键点会被压缩EAR 的整体数值变小仰拍时鼻孔和下巴的关键点干扰严重。我的经验是摄像头对准面部正面高度略高于眼睛距离 50 到 70 厘米这个范围内 dlib 的 68 点定位最稳。光线方面opencv 图像处理里最常用的一招是直方图均衡化。夜间或逆光时灰度图对比度低人脸检测器容易漏检。在送进 detector 之前对灰度图做一次均衡化能明显提升夜间检出率gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # CLAHE 比普通直方图均衡化更温和不会把噪点放大 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray clahe.apply(gray)clipLimit2.0 表示对比度限制太高会让画面出现块状伪影。这个操作对白天画面影响不大但对夜间场景是救命级别。报警策略上我发现声音报警比弹窗有效得多驾驶场景里没人会盯着屏幕看。后面第六章会写具体怎么接声音这里只需要记住一个原则报警输出要和疲劳判据解耦判据只管算疲劳程度报警模块单独控制音量、持续时间和冷却。5. 疲劳驾驶检测避坑指南五个翻车现场与解法5.1 dlib 模型加载失败程序一跑就崩现象程序启动时报 RuntimeError提示 Unable to open shape_predictor_68_face_landmarks.dat或者启动正常但检测到人脸时直接崩溃。原因前三名分别是路径不对、模型文件未下载完整、文件名大小写不一致。网上有些源码包里的模型文件名和代码里写的字符串差一个下划线肉眼很难发现。dlib 本身不校验模型文件是否完整它只是在 predict 时突然访问到非法内存。解决首先用绝对路径替换相对路径见 3.3 节的做法。然后检查模型文件大小必要时重新下载一次并校验压缩包解压后的字节数。最后把模型放进项目的 models 目录不要放在桌面或下载文件夹里指望代码能找到。这个坑我踩过不止一次现在每个项目里都固定写一个 load_model() 函数失败时把错误信息直接打在屏幕上省得猜。5.2 摄像头打不开或画面严重卡顿现象cap.read() 一直返回 False或者画面像幻灯片一样一帧一帧跳。原因索引选错是第一位。笔记本内置摄像头是 0USB 外接摄像头是 1但有些设备多个摄像头索引不连续要多试几个数字。第二位是分辨率设置过高dlib 检测本身就吃 CPU如果再把分辨率拉满帧率会从 30 掉到个位数。解决先跑一段最小探测脚本循环尝试 0 到 3 的索引看到哪个能出画面就用哪个。然后把 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)、cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) 固定住。640×480 分辨率加 detector(gray, 0) 没有上采样在普通 i5 处理器上能跑到 20fps 左右这是“能用”的下限。如果还卡就需要引入跳帧处理和关键点跟踪而不是继续压分辨率。5.3 戴眼镜和低头时误报飙升现象不戴眼镜一切正常戴眼镜后闭眼检测频繁触发或者司机低头看手机时系统疯狂报警。原因镜片反光会让 dlib 关键点在镜框位置漂移眼睛区域被压缩EAR 数值异常偏低。低头时人脸检测框变小68 点定位分布变形眼睛和嘴巴的坐标错位误判成闭眼。解决EAR 平滑窗口从 5 帧加大到 8 帧先把短期抖动压住。然后头部姿态判断用 68 点中的鼻尖和下巴轮廓算俯仰角如果低头角度超过 30 度直接不参与疲劳判断因为这时候本来就看不到眼睛。这个逻辑用简单几何就能实现不必上复杂模型但要在论文里单独说明“低头状态”与“闭眼状态”的区分依据不然答辩会问到你词穷。5.4 夜间和逆光场景检测率大跳水现象白天能达到 95% 的检测率晚上开近光灯后人脸上半部全是噪点关键点定位结果上下乱跳。原因灰度图在低照度下信噪比急剧下降dlib 的 HOG 特征对梯度信息敏感噪点会制造大量虚假梯度人脸框要么找不到要么框的位置不稳定。这是传统视觉方法的天花板不是参数能完全救回来的。解决先把 3.3 节的 CLAHE 均衡化加上这套方案能救回大部分夜间场景。然后检查画面里是否有一块特别亮的区域比如仪表盘反光用感兴趣区域ROI把画面上方 30% 裁掉减少光源干扰。如果摄像头支持红外补光优先开红外夜视模式。最后给系统加一条保险逻辑连续 5 秒检测不到人脸时不再尝试报警而是记录一条“检测失效”日志事后分析时总比瞎报警好。5.5 数据标注质量差评估指标虚高现象代码跑通后精度看着有 95% 以上但换一段真实视频测试误报多到没法用。原因数据里的“闭眼”标注本身就不准确。有些标注者把半闭眼都标成闭眼有些把打哈欠的瞬间标成闭眼模型的评估指标当然好看因为它在用同样的偏见评估自己。这就是“数据决定了上限”的典型例子算法只是在这份数据上找规律。解决拿到全部数据后先抽 100 帧人工复核看标注标签和实际画面是否符合常识。重点检查三类易混淆样本戴墨镜的、低头露出半张脸的、逆光看不清眼睛的。如果一个类别里这种样本占比超过 20%建议重新标注而不是直接拿去过训练。另外做评估时不要只看总精度把“眨眼召回率”和“正常驾驶误报率”分开算后者才是答辩时最会被追问的指标。6. 把毕业设计改成实用检测器三条进阶路径6.1 把 USB 摄像头更换到视频文件和 RTSP 拉流很多同学答辩演示时用笔记本摄像头但真实场景里检测器要接车载摄像头画面。把 VideoCapture 的入参换成文件路径或 RTSP 地址就能完成切换# 本地视频回放适合复现实验 cap cv2.VideoCapture(fatigue_test.avi) # 局域网摄像头 RTSP 推流注意拉流中断后的自动重连 cap cv2.VideoCapture(rtsp://192.168.1.64:554/onvif1)RTSP 拉流最常遇到的问题不是图像清晰度而是断流。摄像头重启、网络抖动都会让 read() 永久返回 False必须在循环里加超时重连否则演示现场直接黑屏。后续再加强就是多线程拉流把读帧放到独立线程避免网络等待卡住主循环。6.2 用声光报警替代控制台打印控制台打印只有你自己看得见真正的报警要能提醒司机。Windows 上用 winsound 是最短路径import winsound if perclos 0.2: winsound.Beep(1000, 2000) # 频率 1000Hz持续 2 秒Beep 的频率选 800 到 1000Hz太低没穿透力太高刺耳。更完整方案是用树莓派加蜂鸣器和 LED把触发信号输出到 GPIO那就离开 pure Python 的边界了但毕业设计能加这一层明显是加分项。6.3 重新评估 PERCLOS 阈值给自己的数据留余地最后一步别偷懒。用自己录制的 10 段视频每段 1 分钟分别记录正常驾驶和模拟疲劳状态跑一遍检测器算出误报率和漏报率画出闭眼 EAR 分布的直方图然后重新定阈值。这个过程会让你的论文不再是“复现”而是“基于特定摄像头安装位置的标定实验”省得答辩时被一句话问住。我最早接手这类项目时第一件事就急着调参结果被数据标注坑了一把用了好几天优化 EAR 阈值最后发现是标注时间戳对不上。后来养成习惯先跑通、再标定、最后上报警。这个顺序值得保留希望帮到你。本文还有配套的精品资源点击获取
返回列表