ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

OpenCV与dlib实现疲劳驾驶检测:人脸关键点与EAR阈值实战

OpenCV与dlib实现疲劳驾驶检测:人脸关键点与EAR阈值实战 简介基于OpenCV和Dlib实现的疲劳驾驶检测系统完整工程面向计算机视觉入门者、智能座舱开发者以及安全驾驶相关课题研究者用于实时监测驾驶员的眨眼、闭眼和头部姿态及时发出疲劳预警。系统以Python编写将OpenCV的图像处理能力与Dlib的人脸关键点检测能力相结合形成从摄像头采集、人脸检测、眼睛纵横比计算到疲劳判断的完整流程。压缩包体积约93.53MB共包含21个文件类型覆盖Python源码、预训练人脸关键点模型.dat、人脸检测配置.xml、可视化检测页面.html、测试图像.png/.jpg以及演示动画.gif等基本涵盖模型、代码、数据与展示四个层面。资源包中的文件按代码、模型、图片等分类组织便于直接定位使用。目前该资源已有422人浏览学习对于希望快速跑通一个疲劳检测项目的读者而言获得整套工程后可以直接运行调试结合源码理解算法原理也可替换模型或修改参数进行二次开发适合课程设计、毕业设计和技术验证。1. 疲劳驾驶检测系统拆解OpenCV dlib 能做哪些事凌晨两点长途司机连续开了四个小时眼睛开始不自觉地往下掉。装在驾驶室的摄像头拍到闭眼过程时如果系统能在眼睛闭合的那一秒做出判断提前报警就可能避免一次事故。这套基于 OpenCV 和 dlib 的疲劳驾驶检测系统就是用普通摄像头抓帧dlib 检测 68 个人脸关键点OpenCV 做画面处理最后用眼睛纵横比和嘴巴纵横比判断打瞌睡、打哈欠。它不依赖眼球追踪仪也不依赖脑电手环一台笔记本加一个 USB 摄像头就能搭出实时原型。适合正在入门 OpenCV、需要完成毕业设计或者想给公司做低成本安全监控方案的工程师。下面按我拆这套项目的顺序先解决环境再谈关键点计算、疲劳判定和几个翻车点。2. 环境搭建OpenCV 与 dlib 的版本搭配和安装顺序先说结论整个项目最花时间的地方不是算法而是把 dlib 装进你的 Python 环境。OpenCV 一条 pip 命令基本能搞定dlib 在 Windows 上经常要现场编译一条命令走不完。我的习惯是先建虚拟环境再装 OpenCV最后装 dlib顺序反了可能出现 OpenCV 和 dlib 各自依赖的 Python 版本冲突。2.1 先建虚拟环境Python 3.8 是当前兼容性最稳的选择用 conda 或 venv 都行。这套代码里主要用的是 OpenCV 的 VideoCapture、cvtColor、imshow还有 dlib 的正脸检测器和 68 点预测器。这些库对新版本 Python 的支持有滞后性特别是 dlib 的预编译 wheel 并不是所有 Python 版本都覆盖。常见做法是建一个 Python 3.8 的环境原因很朴素OpenCV 对 3.8 的二进制包最全dlib 的社区轮子也优先打 3.8 和 3.9。conda create -n fatigue_detect python3.8 -y conda activate fatigue_detect建环境的动作不复杂但我见过不少人跳过这一步直接用系统 Python 装一堆依赖结果 opencv-python 和另一个项目里的 opencv-contrib-python 互相覆盖连 import cv2 都可能拿到旧的版本号。虚拟环境相当于后悔药等系统环境被搞坏再回头重建成本高得多。python3.8 不是绝对的如果你手头平台只有 3.9也可以继续后面安装 dlib 时多留意报错即可。环境激活后先用一条命令确认 pip 指向的是当前环境python -m pip --version如果显示的路径不是 fatigue_detect 里的 Python多数情况是 IDE 里解释器没切。这个细节后面避坑章还会提到现在先把这个习惯落实。2.2 安装 OpenCVopencv-python 和 opencv-contrib-python 的区别正常情况下装一个 opencv-python 就够了。项目用到的函数都在主包里。如果你还需要 SIFT、ORB 这些在 opencv_contrib 里的扩展功能才需要装 opencv-contrib-python但疲劳驾驶检测用不到所以别图省事同时装两个很容易出现符号冲突。pip install opencv-python4.8.1.78为什么固定版本号因为 4.8.x 是当前比较稳的稳定版OpenCV 5.x 还没有正式发布贸然升到最新版部分函数行为有变化代码本身能跑但一些小参数会让你莫名其妙。固定版本也能保证团队里其他同事复现时看到的是同一个行为。安装完验证python -c import cv2; print(cv2.__version__)如果输出了类似 4.8.1说明没问题。如果这里报 ModuleNotFoundError: No module named cv2先检查你当前是不是在疲劳检测的虚拟环境里不要在全局环境里验证。另一个常被忽视的点Linux 服务器上如果没有图形界面import cv2 之后一旦调用 cv2.imshow 就会崩溃这时应该用 opencv-python-headless并把代码里所有 imshow、waitKey 相关分支去掉改成写文件输出。做远程测试时我一般直接不显示窗口把每帧检测结果打印到控制台。2.3 dlib 安装一条 pip 命令为什么在 Windows 上不够用dlib 不是一个纯 Python 库它的核心是 C 模板库pip 安装时会尝试本地编译。在 Linux 上编译前提是 build-essential 和 cmake在 macOS 上需要 Xcode 命令行工具在 Windows 上需要 Visual Studio Build Tools 里的“使用 C 的桌面开发”工作负载加 CMake。任何一个缺失都会在编译到一半时抛出一堆看不懂的红字最常见的两类错误是 CMake 版本不满足、Boost 找不到。我的建议分两步走先在环境里试pip install dlib如果报编译错立刻换 conda 预编译包不要在同一个坑里硬磨半小时。pip install dlib如果失败执行conda install -c conda-forge dlibconda-forge 里通常有对应平台编译好的 dlib 二进制省去本地编译。装完执行python -c import dlib; print(dlib.__version__)确认能 import。这里有个细节输出正常后不要急着往下走先运行一个简单的人脸检测确保没有缺少 libX11 之类的依赖。否则后面加载模型时才暴露问题更难受。2.4 准备 68 点模型文件shape_predictor_68_face_landmarks.dat算法部分依赖 dlib 官方训练好的 68 点人脸关键点模型文件名是shape_predictor_68_face_landmarks.dat。这个文件大概一百多兆从 dlib 官网模型列表里能下载。下载后不要放在中文路径下Windows 下 Python 对中文路径的支持偶尔会抽风常见做法是放在项目的models/目录里路径用相对路径。import dlib # 加载模型路径里不要出现中文 predictor dlib.shape_predictor(models/shape_predictor_68_face_landmarks.dat) print(predictor loaded)代码逻辑很简单但它的加载过程会校验文件头。如果文件下载不完整这里会抛异常或者加载成功后预测出来的点全是 (0,0)。所以在正式编写数据流程之前先单独加载一次模型并打印一行确认信息能省掉后面排查的大量时间。我一般会顺手打印predictor对象本身确认它不是空引用。到这里环境侧的工作已经闭环OpenCV 提供图像读写和显示dlib 提供人脸检测与关键点预测。接下来一章节讲的是整个系统真正的数据入口怎么拿到一帧画面里人脸的 68 个点。3. 人脸检测与 68 点关键点从 HOG 到 dlib 坐标解析3.1 dlib 正脸检测器为什么不用 OpenCV 的 Haar Cascade对人脸检测OpenCV 自带 Haar Cascade也能框住正脸。但 dlib 的get_frontal_face_detector()基于 HOG 特征加线性 SVM对侧脸和光照变化的容忍度比 Haar 好而且和后面的shape_predictor是同一套生态直接喂检测框省了不同库之间的坐标系转换。疲劳驾驶场景里光线不是理想环境我一般直接选 dlib 检测器OpenCV 只负责摄像头读取和图像处理。代码里先把检测器、预测器、摄像头初始化import cv2 import dlib detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(models/shape_predictor_68_face_landmarks.dat) cap cv2.VideoCapture(0, cv2.CAP_DSHOW) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) if len(faces) 0: shape predictor(gray, faces[0]) for i in range(68): x shape.part(i).x y shape.part(i).y cv2.circle(frame, (x, y), 2, (0, 255, 0), -1) cv2.imshow(face landmarks, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明读到的 frame 通常是 BGRdlib 的检测器接受 8bit 灰度图所以先用 cvtColor 转灰。detector 返回的是dlib.rectangles里面每个矩形是人脸框。把它直接传给 predictorpredictor 返回dlib.full_object_detection里面 68 个关键点的坐标通过part(i).x和.y访问。这里detector(gray, 0)的第二个参数是图像上采样倍数0 表示不做放大速度快调成 1 会在检测前放大一倍小脸更容易被找到但速度会掉一大截。做疲劳检测时司机脸部通常离摄像头不太远设置为 0 更合适。另外cv2.CAP_DSHOW只在 Windows 下生效它的作用是让 VideoCapture 走 DirectShow 后端解决笔记本摄像头打开慢、索引不稳定问题。Linux/macOS 上这个参数会被忽略保留也不影响编译。3.2 68 点索引表眼睛、眉毛、嘴巴的编号和坐标验证dlib 的 68 点模型遵循固定的编号排序脸部轮廓 0-16眉毛 17-26鼻子 27-35眼睛 36-47外嘴唇 48-59内嘴唇 60-67。这里的左右眼是面向摄像头时候的左右和你从屏幕看到的左右刚好相反这是第一个容易错的地方。区域索引范围用途右眼画面左侧36–41计算右眼纵横比左眼画面右侧42–47计算左眼纵横比外嘴唇48–59计算嘴巴纵横比内嘴唇60–67可做更精细的哈欠判断注意表格里右眼指的是人的右眼出现在画面左边左眼是人的左眼出现在画面右边。很多人在写代码时把 36-41 当作左眼用结果 EAR 曲线完全反着走。拿到 shape 之后我一般会把每只眼的六个点单独打印出来确认是不是紧贴着眼眶。代码可以这样写def get_eye_points(shape, indices): points [] for i in indices: x, y shape.part(i).x, shape.part(i).y points.append((x, y)) return points left_eye_indices list(range(42, 48)) right_eye_indices list(range(36, 42)) left_eye get_eye_points(shape, left_eye_indices) right_eye get_eye_points(shape, right_eye_indices)这段代码做的事情是把编号转成坐标列表。为什么用 list(range(42,48)) 而不是写死六个数字因为后面计算 EAR 时需要按顺序访问 p0-p5而 dlib 的索引本身就是从眼角外侧开始顺时针排列用 range 生成可以少记六个数字。逻辑上左眼 42-47 是以人脸的左眼为准如果你看到画面上右边那只眼的连接线画得奇怪就把 36-41 和 42-47 对调。3.3 一个容易被忽略的边界检测不到人脸时怎么办在驾驶场景里低头、侧头、光线过暗都会让正脸检测器返回空列表。如果直接跑faces[0]会抛 IndexError整个视频循环崩掉。上面代码里已经用if len(faces) 0挡了一下但更好的做法是维护一个“上一帧关键点存在”的状态。当这一帧没有检测到人脸就沿用上一帧的 EAR 值或直接判定为闭眼状态。从工程上讲你不能让人脸偶尔丢失导致整个报警系统闪烁。实际代码里可以这样last_ear 0.25 if len(faces) 0: shape predictor(gray, faces[0]) # 这里计算最新 EAR 并刷新 last_ear else: # 使用 last_ear 继续跑阈值判断避免闭眼计数清零 pass这个保存历史值的模式在第四节实现时很有用。因为疲劳判断是“连续若干帧闭眼”中间突然丢一帧人脸不应该让闭眼计数清零。我一般把 last_ear 放在循环外每次检测到人脸才更新。这种细节决定了系统在真实驾驶室里能不能扛过偶尔的人脸丢失而不是在演示时表现良好。4. 疲劳指标计算从 EAR 到 PERCLOS 的阈值与判定逻辑4.1 EAR 不是玄学六个点算出的眼睛开合度眼睛纵横比EAR的概念来自 Soukupová 的论文它的计算方式是取眼睛周围六个关键点分别计算竖直距离与水平距离的比值。当眼睛睁开时竖直距离 A、B 占水平距离 C 的比例约在 0.25-0.35闭眼时比值会掉到 0.1 以下。用距离比值而不是绝对像素好处是摄像头远近变化时不会被像素值骗到。import numpy as np def euclidean_dist(point1, point2): return np.linalg.norm(np.array([point1.x, point1.y]) - np.array([point2.x, point2.y])) def eye_aspect_ratio(shape, eye_indices): # 按顺时针顺序取出六个点 p [shape.part(i) for i in eye_indices] A euclidean_dist(p[1], p[5]) B euclidean_dist(p[2], p[4]) C euclidean_dist(p[0], p[3]) return (A B) / (2.0 * C)参数说明eye_indices 传 42-47 或 36-41顺序必须保持顺时针。A 是上下两点1和5的垂直距离B 是第二组上下点2和4的垂直距离C 是内眼角到外眼角的宽度。EAR 是垂直整体除以水平宽度眼睛越闭越小。判断闭眼时不能单看一只眼我会把左右眼的 EAR 分别算出来取较小值因为人有习惯性单眼眯着的情况左边睁着右边闭的场景平均值会丢掉右眼闭眼信息。4.2 嘴巴纵横比 MAR区分打哈欠与说话嘴部区域在第 48-67 号点外嘴唇 48-59 号点围成一圈。打哈欠时嘴会张得很大张嘴说话时也有开度但持续时间很短。用 48 和 54 两点作宽度50 和 58 两点作高度高度/宽度就是简单的嘴部纵横比。def mouth_aspect_ratio(shape): p48 shape.part(48) p50 shape.part(50) p54 shape.part(54) p58 shape.part(58) width euclidean_dist(p48, p54) height euclidean_dist(p50, p58) return height / width逻辑说明正常闭着嘴时 width 远大于 height比值大概在 0.2 以下打哈欠时 height 变大比值可能超过 0.5。说话时也会冲到 0.5但不会持续太长时间。所以哈欠报警在代码里需要加一个“连续 N 帧 MAR 大于阈值”的时间窗口我一般取 0.5 和 5 帧避免把一句“哦”误判成哈欠。4.3 疲劳判定的时间窗口连续闭眼帧数与 PERCLOS单帧 EAR 低于阈值不能报警因为正常眨眼也会让 EAR 短时间掉到阈值以下。正常人每分钟眨眼 10-15 次一次闭眼约 100-150ms。疲劳的典型表现不是眨眼次数减少而是单次闭眼时间变长闭眼持续时间超过 0.3-0.5 秒就需要报警。如果摄像头是 25fps0.4 秒大约对应 10 帧。EAR_THRESHOLD 0.25 CLOSED_FRAME_LIMIT 8 closed_frames 0 while True: # 读取帧检测人脸计算左右眼 EAR ear min(left_ear, right_ear) if ear EAR_THRESHOLD: closed_frames 1 if closed_frames CLOSED_FRAME_LIMIT: alarm True else: # 眼睛睁开重置闭眼计数 closed_frames 0参数说明EAR_THRESHOLD 是静态闭眼阈值CLOSED_FRAME_LIMIT 是连续闭眼多少帧报警。这两个值一定要实测微调。戴眼镜、眯眯眼、光照差都会让 EAR 整体偏高或偏低打印实时 EAR 值观察后再定。代码里把 alarm 置为 True 后通常还要再接一段声音播放或保存证据帧的逻辑。报警之后不要在同一帧立刻复位 closed_frames否则系统会一直是报警-复位-报警的循环停不下来。PERCLOS 是另一个常用指标含义是单位时间内眼睛闭合时间占比。比如统计 30 秒里 EAR 低于阈值的帧数除以总帧数超过 20% 就认为疲劳。在驾驶场景里我会把 PERCLOS 做成一个滚动窗口每秒计算一次而不是从头累计到 30 秒。窗口太长响应慢窗口太短容易受单次眨眼干扰。实测里 10 秒窗口、20% 阈值比较合理。from collections import deque closed_ratio_buffer deque(maxlen250) # 每检测一帧 closed_ratio_buffer.append(1 if ear EAR_THRESHOLD else 0) if sum(closed_ratio_buffer) / len(closed_ratio_buffer) 0.2: fatigue_level high逻辑说明deque(maxlen250) 在 25fps 下恰好是 10 秒最老的帧自动出去新的进来队列里 1 的占比就是 10 秒内闭眼比例。这样疲劳状态不是一条简单的持续上升计数器而是能反映最近一段时间的行为状态。用这个窗口值配合连续闭眼帧数误报比单独用固定帧数少很多。4.4 报警输出与画面叠加报警提示要直观我一般直接在帧上用红色字写 FATIGUE同时把实时 EAR、MAR、fps 打在画面左上角。这样在调试阶段能一眼看到阈值选得是否合适。cv2.putText(frame, fEAR: {ear:.2f} MAR: {mar:.2f}, (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) if alarm: cv2.putText(frame, FATIGUE ALERT, (100, 100), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3)参数说明putText 的坐标系以画面左上角为原点字体、颜色、粗细都可以直接调。显示实时值不是为了好看而是后面避坑章里标定阈值的重要工具。报警逻辑可以做在同一个循环里也可以用多线程单独跑语音文件避免阻塞视频帧读取。5. 避坑指南五个常见失败现场与排查顺序5.1 dlib 安装失败CMake 和 Boost 报错现象pip install dlib 在 Windows 上编译到 80% 时报错 “Could NOT find Boost” 或者 “CMake error at CMakeLists.txt”。原因系统缺少 Visual Studio Build Tools 的 “使用 C 的桌面开发” 工作负载dlib 的 CMake 配置找不到 Boost。解决打开 Visual Studio Installer添加该工作负载后重启电脑再重新 pip 安装。如果前后配置没把握直接改走 conda-forgeconda install -c conda-forge dlib我遇到过一个更隐蔽的情况电脑里装了 Visual Studio 但是没有勾选 C 工作负载pip 报错时显示找不到编译器。先不要把锅甩给库用where cl检查命令是否存在。另外 CMake 版本也别太低老版本无法识别 dlib 的 CMakeLists 中的新写法直接升级到最新随身版即可。5.2 闭眼 EAR 却不降关键点画到了眼皮外面现象眼睛闭着EAR 还是 0.3 甚至 0.4报警永远不触发。原因左右眼索引写反了。dlib 模型的 36-41 是人的右眼42-47 是人的左眼。如果把 42-47 当画面左眼处理画出来的六个点连成一个歪的六边形EAR 当然不对。解决把关键点可视化出来确认每个编号落在眼眶边缘。我现在的习惯是拿到任何关键点模型先运行一次“画点连线”的脚本绝不想当然地相信索引表# 把所有眼睛相关点都画出来 for i in range(36, 48): x, y shape.part(i).x, shape.part(i).y cv2.circle(frame, (x, y), 2, (0, 255, 0), -1)运行后看哪只眼出现在画面的左/右再对照你的左右眼变量名。还有一个连带现象单只眼 EAR 正常另一只眼 EAR 恒为 0.5 左右多半是眼皮上的点被误判到了眉毛上这时要检查是不是戴了粗框眼镜导致检测点偏移。5.3 帧率掉到 10fps人脸检测参数与连续检测陷阱现象系统能跑但画面明显卡顿fps 只有 10 左右。原因detector(gray, 1)的上采样放大倍数设为 1人脸检测的计算量几乎是 0 的好几倍另外每一帧重新做整图检测本身也比关键点预测耗时。解决把第二个参数改成 0如果还卡采取“每 10 帧重新检测一次人脸中间帧用上一次的人脸框”或者改用 dlib.correlation_tracker 跟踪上一次位置。注意 tracker 和 predictor 的搭配先 track 再 predict因为 predict 需要准确的人脸矩形。实测 30fps 摄像头上这个改动能让系统回到 25fps 以上。# 每 10 帧做一次完整检测 if frame_count % 10 0: faces detector(gray, 0) if len(faces) 0: tracker.start_track(gray, faces[0])tracker.start_track 只能初始化一次后续要调用 tracker.update(gray)。我一般把 frame_count 放在视频循环里第一帧先 start_track中间帧 update这样既保留了检测精度又避免每帧都跑完整人脸检测。5.4 摄像头打不开或画面是黑的索引和 DirectShow 后端现象代码在 A 机器上能开摄像头换到 B 机器上VideoCapture(0)返回 True 但画面漆黑或者 cap.isOpened() 一直 False。原因笔记本有内置和外接两个摄像头索引 0 被占用为其他虚拟设备Windows 下 OpenCV 默认的 VFW 后端兼容性差。解决用一个循环探测 0-5 号索引选第一个能打开且返回有效帧的设备Windows 下使用cv2.CAP_DSHOW后端cap cv2.VideoCapture(0, cv2.CAP_DSHOW)如果画面还是黑的可能是摄像头被别的软件占用关闭微信、浏览器摄像头权限后重试。在 Linux 上还要检查 /dev/video0 的权限常见做法是把当前用户加入 video 组。5.5 模型文件加载异常或预测点全是 0现象加载shape_predictor_68_face_landmarks.dat时不抛错但预测出来的 68 个点全是 (0,0)或者加载时抛 “Invalid shape predictor file”。原因模型文件损坏下载不完整路径包含中文让 ifstream 读出了错。解决检查文件大小是否为 99.7MB 左右把模型放到纯英文路径重新下载后覆盖旧文件。任何一次 load 后先打印shape.part(30).x验证是否为非 0。这条在之前的章节里提过但它是最容易在交付现场出现的问题值得单独记一条拿到新环境先加载模型再跑流程。如果检查完都没问题但预测点仍然偏移再看分辨率过小的输入图像会导致关键点回归失败我一般把帧缩放到宽度 640 再送给 predictor。6. 让系统在实车上跑实时平滑与阈值标定技巧6.1 用 EMA 平滑 EAR别让单帧抖动干扰判断EAR 是坐标像素算出来的摄像头噪点、面部肌肉轻微拉动都会让单帧值上下跳。直接拿原始值与阈值比较报警可能在边界值附近反复横跳。我一般在进判定之前做一次指数滑动平均alpha 取 0.3 左右。smooth_ear 0.0 # 每帧更新 smooth_ear alpha * ear (1 - alpha) * smooth_earalpha 越小曲线越平滑但反应越迟钝太大又骗不了噪声。0.3 是我实测驾驶场景下比较折中的值。6.2 验证技巧把关键点、EAR、FPS 画在同一帧画面上拿到任何摄像头项目我的习惯是先开一屏三显画面左上角打印 fps左下方打印实时 EAR 和眼睛状态同时把所有关键点画出来。然后对着摄像头做几个动作睁眼、闭眼、打哈欠记录睁眼时的 EAR 范围和闭眼时的范围用这两个范围去定阈值而不是直接抄别人代码里的 0.25 和 0.5。cv2.putText(frame, ffps: {fps:.1f} ear: {smooth_ear:.2f} mar: {mar:.2f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)逻辑说明fps 用最近 30 帧的平均耗时算直接向前端展示。阈值标定之后再验证两件事连续眨眼 10 次不应该触发报警人为闭眼超过 0.5 秒一定要触发报警。这套资源里算法部分已经包好拿到手最重要的是把自己的摄像头环境调通别在没做完可视化验证前就上真车。那次我在实验室里调误报眼看着屏幕上 EAR 在 0.22 到 0.28 之间来回动阈值怎么改都拦不住误报。后来把关键点和实时值打印出来才发现摄像头自动曝光把亮度拉得太低六个关键点有两三个跑到眼皮外面去了。从那以后我每次接手摄像头检测项目都强制先走一遍“画关键点、打 fps、打实时指标”的流程再谈阈值和报警。希望帮到你。本文还有配套的精品资源点击获取
返回列表