ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于dlib 68点关键点的Python驾驶员疲劳检测系统实战

基于dlib 68点关键点的Python驾驶员疲劳检测系统实战 简介一套面向个人学习者的驾驶员疲劳检测系统以图像处理和计算机视觉识别面部关键点可实时判断眨眼、打哈欠、瞌睡点头三类疲劳行为适合正在接触人脸检测、特征点定位或小型视觉项目的人群。整个包共21个文件约85.77MB源码、模型、界面、演示一应俱全包括可阅读修改的py脚本与Jupyter示例、预训练的人脸68点模型、Windows下直接运行的exe安装程序、效果演示视频以及界面设计源文件便于完整跑通项目。判定规则明确连续3帧内眼睛长宽比为0.2视为眨眼嘴部长宽比为0.5视为打哈欠头部pitch旋转角为0.3视为瞌睡点头读者可据此理解算法阈值并自行调优。已有55人学习下载特别适合用于课程设计、毕设或自学练兵在动手过程中熟悉Python图像处理、模型加载和实时状态判断的完整链路。1. 从三帧画面判断犯困这套疲劳检测系统到底在算什么跑长途的老司机最怕的不是路况而是眼睛不自觉地闭上那一两秒。这套基于图像检测的 Python 驾驶员疲劳检测系统做的就是盯住摄像头画面里的面部关键点用三个硬指标来认定疲劳状态连续三帧内眼睛长宽比低于 0.2 判定为眨眼嘴部长宽比高于 0.5 判定为打哈欠头部 pitch 旋转角达到 0.3 判定为瞌睡点头。整套代码跑在 dlib 的 68 点面部关键点模型之上附带了测试视频、模型文件和安装程序不需要自己从头训练模型。对想入门计算机视觉的个人学习者来说它是少有的能直接跑起来、能改参数、能观察误报的完整实战素材而不是那种只剩半截核心算法的演示片段。2. 三个判定标准背后的数学EAR、MAR、Pitch 角从公式到代码2.1 68 点模型一张脸被拆成 68 个坐标点这套系统最底层的地基是 dlib 的shape_predictor_68_face_landmarks.dat模型文件。它接受一张含有人脸的图像输出 68 个关键点的坐标每个点对应面部的特定位置比如 1 号到 17 号是下巴轮廓37 点到 48 点是双眼区域49 点到 68 点是嘴巴区域。项目里所有疲劳判断都建立在这组坐标之上没有它后面的长宽比、旋转角都无从谈起。加载模型的代码在项目里很固定一般在main.py或main_UI.py的头部import dlib # 初始化检测器和特征点预测器 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) # 读取一帧图像 img cv2.imread(images/test.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 检测人脸框 faces detector(gray, 0) for face in faces: # 取 68 个关键点 landmarks predictor(gray, face)逻辑说明detector负责找到人脸的外接矩形框predictor在框内定位 68 个关键点。detector的第二个参数是放大系数0 表示不做图像金字塔放大处理速度更快换成 1 可以稍微提升小尺寸人脸的召回率但帧率会下降。参数说明shape_predictor_68_face_landmarks.dat是 dlib 官方预训练好的模型约 99MB本项目压缩包里已经带了一份。如果你的路径不对会直接抛异常后面避坑章节我会单独说。2.2 眨眼判定连续三帧 EAR 低于 0.2眼睛长宽比EAREye Aspect Ratio是被广泛用于困倦检测的经典指标。68 点模型中每只眼睛周围有 6 个关键点分别对应眼睑的上下左右位置。EAR 的分子是两组垂直方向欧氏距离的平均值分母是水平方向的欧氏距离。当眼睛睁开时垂直距离和水平距离的比例相对稳定当眼皮垂下垂直距离迅速变小EAR 就会骤降。eye_detecting.py的核心逻辑如下from scipy.spatial import distance as dist def eye_aspect_ratio(eye): # 垂直方向两组关键点的欧氏距离 A dist.euclidean(eye[1], eye[5]) B dist.euclidean(eye[2], eye[4]) # 水平方向一组关键点的欧氏距离 C dist.euclidean(eye[0], eye[3]) # 长宽比垂直距离的平均值除以水平距离 ear (A B) / (2.0 * C) return ear逻辑说明eye是六个点的坐标数组顺序必须和 dlib 输出一致左眼取 37 到 42 号点右眼取 43 到 48 号点。传入顺序错了EAR 算出来就毫无意义。所有人的眼睛睁开时 EAR 大约在 0.25 到 0.35 之间闭眼时逼近 0所以 0.2 是区分闭眼和睁眼的常见经验阈值。参数说明项目里对眨眼的认定不是单帧而是连续三帧 EAR 都低于 0.2 才记一次眨眼。单帧低于阈值很容易是光线变化或人脸姿态导致的瞬时误判连续帧统计用极简的方式等效了一个时间窗口从代码实现角度说这是最省钱也最容易复现的滤波方案。2.3 打哈欠判定嘴部长宽比 0.5 的阈值含义嘴部长宽比MARMouth Aspect Ratio的逻辑和 EAR 完全对称只是采样点换成了嘴部周围的 8 个关键点。垂直方向取两组距离水平方向取嘴唇左右最远点距离。打哈欠时嘴巴张大垂直距离明显拉长MAR 会超过正常说话时的数值项目把阈值定在 0.5。mouth_detecting.py的判定代码def mouth_aspect_ratio(mouth): # 嘴唇上下两组关键点的欧氏距离 A dist.euclidean(mouth[2], mouth[10]) B dist.euclidean(mouth[4], mouth[8]) # 嘴唇水平方向的距离 C dist.euclidean(mouth[0], mouth[6]) # 嘴部长宽比 mar (A B) / (2.0 * C) return mar # 连续帧计数器 MAR_THRESH 0.5 mar_counter 0 if mar MAR_THRESH: mar_counter 1 else: mar_counter 0 if mar_counter 3: yawn_detected True逻辑说明mouth取 49 到 68 号点中嘴唇外轮廓的关键点。判断时不是检测到一次张嘴就报警而是连续三帧超过 0.5 才认定为打哈欠。这样说话、微笑这类短暂张嘴不会误报符合摘要里对疲劳认定标准的描述。参数说明0.5 这个阈值对大多数成年人适用但嘴型偏大或偏小的人需要微调。实际复现时我建议先跑一遍测试视频观察自己或测试视频里说话、哈欠时的 MAR 区间再回头改MAR_THRESH的取值。2.4 瞌睡点头pitch 旋转角 0.3 的近似算法点头检测在项目里由node_detecting.py负责判断依据是头部绕 x 轴旋转的角度即 pitch 角。pitch 角的标准解法是用 OpenCV 的solvePnP将 2D 关键点重投影到 3D 头模上解出旋转向量再换算成角度。但考虑到个人学习场景的资源有限这个项目里用的是关键点比值近似。常见做法是取鼻尖、下巴等高辨识度关键点构建几何关系计算头部垂直方向的位移比例import math def head_pitch_angle(landmarks): # 取鼻尖和下巴两点 nose (landmarks.part(30).x, landmarks.part(30).y) chin (landmarks.part(8).x, landmarks.part(8).y) # 计算竖直方向的距离变化 vertical_dist chin[1] - nose[1] horizontal_dist abs(chin[0] - nose[0]) # 简化 pitch 角估计弧度转角度 pitch_angle math.atan2(vertical_dist, max(horizontal_dist, 1)) return pitch_angle逻辑说明当人低头时鼻尖和下巴在图像中的竖直距离会明显缩短atan2算出的角度随之变化。这个近似算法没有三维空间里的严格物理含义但在正脸朝摄像头的前提下用于识别点头动作的稳定性足够。摘要中提到的 pitch 旋转角 0.3对应约 17 度的头部俯仰角变化这个幅度明显大于正常驾驶时的微调动作。参数说明landmarks.part(30)是鼻尖点landmarks.part(8)是下巴最底端。如果你发现点头不灵敏可以把阈值从 0.3 适当调高但要小心与转头、低头看仪表盘的动作混淆。3. 部署不是玄学Python 环境、依赖安装与完整启动流程3.1 Python 环境选择为什么建议用 3.8 而不是最新版这套项目从代码风格到依赖版本都是围绕 Python 3.6 到 3.8 时代写的。最稳妥的做法是直接装 Python 3.8原因不在项目本身要求多高而在于 dlib 的预编译 wheel 包对 Python 版本的兼容性。Python 3.9 以上的环境里pip install dlib经常需要现场编译而编译 dlib 又依赖 CMake 和 C 工具链一步不对就白等十分钟。环境配置是个人学习者最容易翻车的地方之一我用最保守的步骤来# Windows 下先确认 python 版本 python --version # 建议创建一个独立的虚拟环境避免污染系统 Python python -m venv fatigue_env fatigue_env\Scripts\activate # 在虚拟环境中安装依赖 pip install opencv-python4.5.5.64 pip install numpy1.21.6 pip install scipy1.7.3 pip install dlib19.22.0逻辑说明venv是 Python 自带的虚拟环境工具不依赖第三方包。把项目依赖装进独立环境最直接的好处是哪怕装坏了删掉fatigue_env目录重来一次也不会影响系统里其他 Python 项目。这是血泪经验我第一次复现时直接往全局环境里塞后来装别的库把 numpy 升到 2.x整个项目的图像处理部分全崩了。参数说明opencv-python用来读摄像头、显示画面和基本的图像预处理numpy负责数组运算scipy提供dist.euclidean距离计算dlib提供人脸检测和 68 点定位。如果你的机器是 macOSdlib可以直接pip安装但 Windows 上强烈建议锁定 19.22.0 这个版本它是最容易获得预编译包的版本。装完依赖后可以用一段极短代码验证环境是否正常import cv2 import dlib import numpy as np from scipy.spatial import distance as dist print(OpenCV:, cv2.__version__) print(dlib:, dlib.__version__) print(NumPy:, numpy.__version__)逻辑说明这四行代码不涉及任何模型加载纯粹验证四个关键的 import 是否成功。只要有一行报ModuleNotFoundError就说明对应依赖没装干净不要继续往下跑主程序。3.2 项目文件拆解每个 py 文件负责什么整个压缩包解压后看起来文件很多但真正参与逻辑的只有几个核心文件搞清楚分工之后再去跑程序才不会一头雾水文件职责eye_detecting.py眼睛关键点检测与 EAR 计算mouth_detecting.py嘴巴关键点检测与 MAR 计算node_detecting.py头部姿态估计与点头判定main.py整合三个检测模块运行主逻辑main_UI.py带图形界面的入口绑定按钮和画面Test.ipynb/UIdemo.ipynbNotebook 形式的演示和调试test.mp4官方测试视频用于验证算法效果shape_predictor_68_face_landmarks.datdlib 68 点模型文件noname.py和111.fbp是 wxFormBuilder 生成的 UI 描述文件123.ico是程序图标这些属于界面辅助文件不影响核心检测逻辑。运行的时候直接从main.py或main_UI.py进不要先去翻noname.py那是界面布局代码看不懂不会影响改算法。3.3 运行主程序视频文件与实时摄像头环境就绪、文件确认之后运行方式就非常简单了。先跑视频文件验证算法是否正常再切换到摄像头看实时效果# 激活虚拟环境如果没激活 fatigue_env\Scripts\activate # 运行主程序读取默认视频 python main.py # 或者直接带图形界面运行 python main_UI.py逻辑说明main.py和main_UI.py内部通过 OpenCV 的VideoCapture读取视频源默认可能是test.mp4也可能是摄像头0。如果你想强制使用摄像头通常要改cv2.VideoCapture(0)里的参数0 代表系统默认摄像头1 代表第二个外接摄像头。笔记本内置摄像头一般用 0外接 USB 摄像头有时会落在 1 上。参数说明两个入口的差别只在交互方式。main_UI.py用 wxFormBuilder 生成的面板绑定按钮适合演示和观察实时画面main.py更朴素适合直接读视频文件做结果验证。第一次复现建议先跑main.py看输出日志确认检测逻辑正常后再用 UI 版做效果展示。UI 版在黑盒运行出现问题时排查难度会比命令行版高不少。4. 避坑与排查dlib 装不上、眨眼没反应、摄像头黑屏的五个真实场景4.1 现象Windows 上 dlib 怎么都装不上表现是执行pip install dlib后长时间卡在编译阶段最后报error: Microsoft Visual C 14.0 is required或者干脆下载超时。原因拆成两层一是网络拉取源码包失败二是即使拿到源码Windows 上没有 C 编译工具链dlib 只能现场编译。Python 3.9 以上的版本尤其容易出现因为 PyPI 上缺少官方预编译的 wheel 包。解决的顺序是先用python --version确认版本如果是 3.9 以上建议直接新建 3.8 虚拟环境然后锁定版本号安装pip install dlib19.22.0如果还是失败用 conda 装预编译包是最后的后悔药conda install -c conda-forge dlib4.2 现象程序能跑但对着摄像头眨眼完全没反应表现是画面正常、人脸框正常但把 EAR 阈值打印到控制台发现数值一直在 0.25 到 0.3 之间波动怎么都不低于 0.2。原因是多数人的下意识眨眼速度很快单帧闭眼时间不足三帧。摄像头帧率只有 15 到 20 帧每秒时一次完整的眨眼可能只被捕捉到一帧计数器根本攒不到连续三帧。解决的办法是打印 EAR 日志确认实际值然后做三选一把 EAR 阈值从 0.2 上调到 0.22把连续帧数从 3 降到 2或者换一个帧率更高的摄像头。实测中光线不足会让眼皮阴影加深EAR 本身就偏高补光比调参更有效。4.3 现象检测框乱跳关键点错位表现是人脸框一会在眼睛上、一会在额头上68 个点跟着乱飘EAR 和 MAR 的值忽高忽低误报疯狂触发。原因是人脸检测器对侧脸、低头、遮挡的鲁棒性有限画面里出现多张人脸或半张脸时检测框会来回切换。框架本身是逐帧独立检测的没有对关键点做帧间平滑。解决是在每一帧检测后加一个简单的坐标平滑用历史位置的加权平均替换当前帧位置# 简单的一阶滤波alpha 控制平滑程度 smoothed_landmarks alpha * current_landmarks (1 - alpha) * prev_landmarks逻辑说明alpha通常取 0.7 到 0.9越接近 1 越相信当前帧越接近 0 越相信历史帧。这个滤波要放在predictor输出之后EAR 和 MAR 计算之前否则滤波对最终判定没有任何作用。它做的是坐标域的平滑和时间域的连续帧判定互相补充。4.4 现象摄像头画面黑屏或报设备被占用表现是程序启动后窗口一片漆黑或者直接抛cv2.error: V4L2 ...之类的异常。在 Windows 上另一个常见场景是调试主程序时窗口没关干净摄像头资源没释放第二次运行就报占用。解决的思路是先关掉所有残留的 Python 进程然后在代码里把 VideoCapture 的索引值改掉import cv2 # 依次尝试 0 和 1找到可用摄像头 cap cv2.VideoCapture(0) if not cap.isOpened(): cap cv2.VideoCapture(1)逻辑说明isOpened()返回 False 表示摄像头初始化失败。笔记本内置摄像头和外接 USB 摄像头可能对应不同的索引提前写死 0 只适合部分机型。更稳妥的办法是循环尝试并打印索引号确认哪个设备可用。4.5 现象运行时提示找不到 shape_predictor_68_face_landmarks.dat 或文件路径报错表现是程序在加载模型那行抛出RuntimeError: Unable to open shape_predictor_68_face_landmarks.dat或者报相对路径找不到文件。原因大多是运行目录和文件目录不一致。你在项目根目录外执行python fatigue_detecting-master/main.py时代码里的相对路径shape_predictor_68_face_landmarks.dat是按当前工作目录解析的而不是按 py 文件所在目录解析的。解决的办法是把模型路径改成基于文件位置的绝对路径import os import dlib # 获取当前文件所在目录 base_dir os.path.dirname(os.path.abspath(__file__)) model_path os.path.join(base_dir, shape_predictor_68_face_landmarks.dat) predictor dlib.shape_predictor(model_path)逻辑说明__file__是 Python 内置变量指向当前 py 文件的完整路径。用dirname取目录再拼接模型文件名就能保证无论从哪里启动模型路径都指向正确位置。这是我在自己的项目里养成的习惯凡是带模型文件的程序一律不写裸相对路径。5. 把检测结果接进预警流程参数校准与声音告警的落地改造5.1 组合三个判定函数成一条完整流水线三个模块单独都能跑但实际驾驶场景里需要的是把它们合进同一帧循环里统一汇总状态。这里给出一个可复用的整合思路import cv2 import dlib detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) cap cv2.VideoCapture(0) eye_frames 0 mouth_frames 0 pitch_frames 0 while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) for face in faces: landmarks predictor(gray, face) ear eye_aspect_ratio(get_eye_points(landmarks)) mar mouth_aspect_ratio(get_mouth_points(landmarks)) pitch head_pitch_angle(landmarks) # 连续帧统计 eye_frames eye_frames 1 if ear 0.2 else 0 mouth_frames mouth_frames 1 if mar 0.5 else 0 pitch_frames pitch_frames 1 if pitch 0.3 else 0 # 触发预警 if eye_frames 3 or mouth_frames 3 or pitch_frames 3: print(疲劳状态触发请休息)逻辑说明get_eye_points和get_mouth_points是把 68 点坐标切分成对应区域的辅助函数避免主循环里堆满索引取值。三个计数器只要有一次不满足条件就清零这种设计能防止累加误差确保连续三帧的语义不被断帧污染。参数说明这个循环里真正需要调的是三个阈值。建议跑测试视频时打印每一帧的 EAR、MAR、pitch 数值然后手动标注哪几帧是眨眼、哪几帧是哈欠、哪几帧是点头再回头调整阈值到既不漏报也不误报。5.2 个性化阈值校准别拿默认参数直接上这套系统最大的一个隐性前提是0.2、0.5、0.3 是通用经验值不同人脸型的差异足以让默认参数失效。我复现时的实测教训是我单眼皮EAR 在正常睁眼时只有 0.22 左右离 0.2 的阈值非常近稍微有一点光线变化就误报眨一次眼。而另一个同事嘴型偏大说话时 MAR 就能到 0.45。校准的土办法是录一段 30 秒左右自己正常说话、眨眼、打哈欠的视频然后写一段脚本统计这三类动作下 EAR 和 MAR 的均值区间取中间值作为新阈值。如果不想写脚本也可以在main.py里临时打印 log 来观察。从那以后我每次拿到这类基于面部关键点的检测项目都会强制走一遍单帧单人脸验证先在静态图片上确认 68 点定位准确再进实时循环调阈值。这套流程帮我避开了至少三次把关键点索引搞反的翻车事故。希望以上内容能帮你顺利跑起来也希望你不止跑起来而是能把它改造成真正适合自己脸型和场景的样子。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进