ARTICLE DETAIL

资讯详情

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

基于OpenCV与dlib的驾驶员疲劳检测系统:眨眼、哈欠与点头识别

基于OpenCV与dlib的驾驶员疲劳检测系统:眨眼、哈欠与点头识别 简介一套基于图像检测的Python驾驶员疲劳识别项目面向计算机视觉初学者、本科课设或毕业设计人群用于研究眨眼、打哈欠、瞌睡点头等疲劳行为的自动判定。系统给出了清晰判定阈值连续三帧内眼睛长宽比达0.2视为眨眼嘴部长宽比达0.5视为打哈欠头部俯仰角达0.3视为瞌睡点头方便对照代码逐步理解算法逻辑。压缩包共21个文件约85.77MB包含Python源码、Jupyter Notebook、人脸68点模型、测试视频、界面安装包等类型可覆盖从算法调试到功能展示的完整学习链路。目前已有55人学习下载。学习者可获得完整项目代码、预训练模型、演示视频和运行效果截图并能根据附带说明快速定位眼睛检测、嘴部检测、点头检测等模块便于复现实验、调整阈值深入掌握图像检测与特征比例判定的实用方法。1. 疲劳检测系统到底在检什么三个信号一个摄像头就够做驾驶员疲劳检测最容易踩的误区是一上来就上深度学习、堆数据集。实际在驾驶舱这个固定场景里摄像头位置基本不动、人脸尺度相对稳定传统图像特征反而够用且更稳。这套基于 Python 图像检测的驾驶员疲劳检测系统核心就做三件事通过摄像头实时计算人脸关键点识别打哈欠、眨眼异常和瞌睡点头并在疲劳信号累计到阈值时报警并附安装程序交付。它不需要红外设备不依赖网联车机一个普通 USB 摄像头加一台 Windows 电脑就能跑起来。适合做毕业设计、车队安全管理系统原型或者想入门 OpenCV 与 dlib 关键点检测的开发者——它会把完整的打包流程和运行库问题一并解决而不是只丢给你一段没法交付的源码。2. 三个疲劳信号是怎么变成数值的从生理特征到可计算指标2.1 为什么选 dlib 关键点而不是直接上深度目标检测疲劳检测在工程上有两条常见路线。一条是端到端深度学习采集大量疲劳/清醒人脸图片训练分类器或者用 YOLO 这类目标检测模型直接回归人脸状态。另一条是先做人脸关键点检测再用几何特征做判断。这个项目采用后者原因很现实深度学习方案需要足够多的疲劳样本而且疲劳表现个体差异极大有的人打哈欠嘴张得大有的人只是频繁眨眼。反过来看眼睑开合、嘴巴开合、头部俯仰这三个动作本质上都能用关键点之间的几何关系刻画。dlib 的 68 点人脸关键点检测器在 CPU 上单帧处理约 20 到 30 毫秒对 30 帧率的摄像头完全够用而且模型文件是开放的、离线可跑不依赖任何云端服务。反观遥感图像目标检测里常用的那些深度模型在驾驶舱这种小目标少、人脸尺度稳定的场景里反而是杀鸡用牛刀。2.2 EAR 眼纵横比把眨眼变成一条数值曲线眨眼检测最经典的数值指标叫 EAREye Aspect Ratio眼纵横比。它只用眼睛周围的 6 个关键点左眼是 36 到 41 号点右眼是 42 到 47 号点。计算方式是眼睛纵向两点距离之和除以两倍横向距离EAR (||P37 - P41|| ||P38 - P40||) / (2 * ||P36 - P39||)睁眼时 EAR 稳定在 0.25 到 0.35 之间闭眼时会掉到 0.1 以下。这个比值是归一化的不随摄像头距离远近变化——这是它比直接用像素距离更可靠的原因。正常成年人每分钟眨眼 10 到 15 次单次眨眼持续 100 到 150 毫秒在 30 帧率下对应 3 到 5 帧闭眼状态。如果检测到单次闭眼持续超过 0.5 秒或者单位时间内眨眼频率异常升高就属于疲劳特征。另一个行业公认的指标是 PERCLOS即单位时间内眼睛闭合帧数占比这个后面在动态校准部分会展开。2.3 MAR 嘴纵横比区分打哈欠和说话的关键在持续时长嘴部状态用 MARMouth Aspect Ratio嘴纵横比取内唇轮廓的 8 个关键点即 60 到 67 号点。公式MAR (||P61 - P67|| ||P62 - P66|| ||P63 - P65||) / (3 * ||P60 - P64||)正常闭嘴时 MAR 接近 0.2说话时在 0.3 到 0.5 之间波动打哈欠时会冲高到 0.6 以上。难点在于说话和打哈欠在嘴型上都是张开区分它们靠的是时间维度说话时嘴部开合是连续、周期性的波动MAR 曲线在短时间内起落多次打哈欠是一次持续 3 到 5 秒的大幅度张开MAR 会持续保持在 0.6 以上至少 1 秒。所以工程上不会单看 MAR 瞬时值而是要求它连续超过阈值若干帧才记一次哈欠。有人做过测试如果只用瞬时阈值判断普通聊天场景下每 3 分钟就可能误报一次哈欠加上连续帧数约束后误报率明显下降。2.4 瞌睡点头用鼻尖轨迹的 V 形变化判断点头检测有两条路线。正规做法是用 solvePnP 求解头部姿态欧拉角在 pitch 角俯仰角上检测先低头再抬头的波形但需要相机内参现场部署时不同摄像头的内参都不一致标定反而麻烦。简化做法是直接用鼻尖关键点30 号点的垂直坐标人在正常驾驶时头部垂直位置有小幅抖动但瞌睡点头是一次比较明显的低头-抬头过程鼻尖的 y 坐标会形成一条先持续下移、再持续上移的 V 形曲线。判定逻辑是在 6 到 8 秒的滑动窗口里如果鼻尖下移距离超过图像高度的 8%且随后 2 秒内回到原位置附近就记一次点头。这个简化方案在摄像头俯拍角度下表现稳定但在完全正对角度下点头幅度会被压缩所以安装摄像头时尽量略高于人脸俯拍 15 度左右效果最好。3. 用 OpenCV 和 dlib 把检测跑起来特征函数、主循环与报警逻辑3.1 环境准备Python 版本选对后面少踩一半坑这个项目对环境最挑剔的依赖是 dlib。dlib 的源码是 C 写的在 Windows 上直接 pip install dlib 通常需要本地有 CMake 和 Visual Studio Build Tools很多人在第一步就翻车。我的建议是直接用 Python 3.9配合 dlib 19.24 的预编译 wheel 包可以省掉编译过程。OpenCV 用当前最新的 4.x 即可安装命令pip install opencv-python dlib19.24.0 imutils numpy playsound如果是在 PyCharm 或 VSCode 里配置记得先给项目建虚拟环境。VSCode 里按 CtrlShiftP 调出 Python: Create EnvironmentPyCharm 在 Settings - Project - Python Interpreter 里新建即可。这里有一个经验别图省事直接装到全局 Python 环境里后面打包成 exe 时 PyInstaller 会把所有依赖扫进产物全局环境会带一堆无关包导致安装包体积暴涨。这个项目运行时主要依赖五个模块cv2 负责图像采集和绘制、dlib 负责检测关键点、numpy 做数值计算、playsound 在检测到疲劳时播放提示音。还需要单独下载 dlib 的预训练关键点模型文件 shape_predictor_68_face_landmarks.dat这个文件接近百 MB是整个系统识别能力的来源。3.2 关键点加载与 EAR/MAR 特征计算函数先写最核心的特征计算函数。这段代码是所有疲劳判断的基础逻辑上不复杂但关键点的索引号不能打错left eye 是 36 到 41right eye 是 42 到 47mouth 内轮廓是 60 到 67。import cv2 import dlib import numpy as np def eye_aspect_ratio(eye): # 计算眼睛的纵向距离 vertical_1 np.linalg.norm(eye[1] - eye[5]) vertical_2 np.linalg.norm(eye[2] - eye[4]) # 计算眼睛的横向距离 horizontal np.linalg.norm(eye[0] - eye[3]) # EAR 纵向平均距离 / 横向距离 ear (vertical_1 vertical_2) / (2.0 * horizontal) return ear def mouth_aspect_ratio(mouth): # 计算嘴巴的纵向距离三点取平均 vertical_1 np.linalg.norm(mouth[1] - mouth[7]) vertical_2 np.linalg.norm(mouth[2] - mouth[6]) vertical_3 np.linalg.norm(mouth[3] - mouth[5]) # 计算嘴巴的横向距离 horizontal np.linalg.norm(mouth[0] - mouth[4]) # MAR 纵向平均距离 / 横向距离 mar (vertical_1 vertical_2 vertical_3) / (3.0 * horizontal) return mar这里说明几个关键点索引的对应关系。eye 数组传入的是某只眼的 6 个关键点坐标eye[0] 和 eye[3] 是眼角的两个点eye[1]、eye[2] 是上眼睑的两个点eye[4]、eye[5] 是下眼睑的两个点。为什么纵向距离要取两点之和除以 2是因为眼睑有弧度单点距离容易受噪声影响取平均更稳。mouth 数组同理mouth[0] 和 mouth[4] 是嘴角其余点是上下唇的轮廓点。MAR 公式里除以 3 是因为纵向取了 3 组距离要和横向距离保持相同的量纲尺度。3.3 主循环把三个判定融合成一条疲劳报警逻辑主循环的任务是逐帧读取画面、检测关键点、计算三个特征值、再做状态判定。下面的代码是一个可运行的最小骨架包含了眨眼、哈欠、点头三个维度的判定逻辑# 初始化 dlib 检测器 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) # 常量参数 EAR_THRESHOLD 0.25 # EAR 低于该值判定为闭眼 EAR_CONSEC_FRAMES 3 # 闭眼连续帧数达到该值记一次眨眼 MAR_THRESHOLD 0.6 # MAR 高于该值判定为张嘴 YAWN_CONSEC_FRAMES 30 # 张嘴连续帧数达到该值记一次哈欠 NOD_WINDOW_SIZE 60 # 点头检测窗口帧数 # 状态变量 eye_counter 0 blink_total 0 mouth_counter 0 yawn_total 0 nose_y_history [] nod_total 0 cap cv2.VideoCapture(0) while cap.isOpened(): 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) # 提取关键点坐标 left_eye np.array([(landmarks.part(i).x, landmarks.part(i).y) for i in range(36, 42)]) right_eye np.array([(landmarks.part(i).x, landmarks.part(i).y) for i in range(42, 48)]) mouth np.array([(landmarks.part(i).x, landmarks.part(i).y) for i in range(60, 68)]) nose_tip (landmarks.part(30).x, landmarks.part(30).y) # 计算当前帧特征值 ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 mar mouth_aspect_ratio(mouth) # 眨眼判定 if ear EAR_THRESHOLD: eye_counter 1 else: if eye_counter EAR_CONSEC_FRAMES: blink_total 1 eye_counter 0 # 哈欠判定 if mar MAR_THRESHOLD: mouth_counter 1 else: if mouth_counter YAWN_CONSEC_FRAMES: yawn_total 1 mouth_counter 0 # 点头判定 nose_y_history.append(nose_tip[1]) if len(nose_y_history) NOD_WINDOW_SIZE: nose_y_history.pop(0) # 绘制特征值便于调试 cv2.putText(frame, fEAR: {ear:.2f} MAR: {mar:.2f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(Driver Monitor, frame) if cv2.waitKey(1) 0xFF ord(q): break几个参数的调整逻辑要讲清楚。EAR_THRESHOLD 设为 0.25 是常见默认值但单眼皮或者眼睛比较小的用户可能睁眼时 EAR 就只有 0.22这时系统会一直判定为闭眼。遇到这种情况最简单的办法是在程序启动时让用户正视摄像头 1 秒采样当前 EAR 作为基线然后取基线的 75% 作为动态阈值。MAR_THRESHOLD 取 0.6、持续 30 帧是为了过滤说话时的嘴部动作30 帧在 30 帧率下正好对应 1 秒。摄像头帧率如果不足 30要把这个数相应调低否则哈欠已经打完了程序还没确认。3.4 用视频文件代替摄像头先把逻辑调通再上真机调试阶段不建议一直对着摄像头看来回截图很费劲。我给代码加一个 --video 参数先用录好的视频测试判定逻辑确认无误后再切换到实时摄像头。这个习惯在交付现场排查问题时特别有用用户反馈误报时你可以让他把当时的视频画面导出来离线重放一遍看 EAR 和 MAR 曲线到底怎么走的。离线调试时建议启动一个回调函数把每帧的 EAR、MAR、鼻尖 y 坐标追加到一个 CSV 文件里跑完用 matplotlib 画出来阈值定得合不合理一目了然。这就是黑匣子思想——只看实时画面很难判断是阈值问题还是检测器抖动曲线数据能直接定位到具体环节。4. 做成可交付的安装程序PyInstaller 打包与运行库补齐4.1 打包命令与路径处理sys._MEIPASS 是分发包的命门项目要在别的机器上跑不能指望人家装 Python 环境。标题里说的附安装程序在 Windows 上常见做法是用 PyInstaller 把 Python 脚本打包成 exe再用 Inno Setup 做成标准安装向导。打包的第一步是安装 PyInstallerpip install pyinstaller然后用下面这条命令打包pyinstaller -F -w --add-data shape_predictor_68_face_landmarks.dat;. -i icon.ico driver_monitor.py参数含义-F 表示打包成单个 exe 文件分发方便-w 表示运行时不显示黑色控制台窗口--add-data 的作用是把模型文件打包进 exe 里Windows 下源文件和目标路径之间用分号分隔Linux 和 macOS 用冒号这个分号冒号的区别是打包踩坑的高发区拼错后模型文件会丢失exe 一运行就报错-i 指定图标不做安装程序可以不加。dist 目录下会生成 driver_monitor.exe看起来万事大吉但双击运行大概率会提示找不到 shape_predictor_68_face_landmarks.dat——因为 -F 模式把程序解压到了一个临时目录当前工作目录并不是 exe 所在目录。解决方法是写一个资源路径函数import sys import os def resource_path(relative_path): base_path getattr(sys, _MEIPASS, os.path.abspath(.)) return os.path.join(base_path, relative_path) # 从模型文件加载改为通过 resource_path 加载 predictor dlib.shape_predictor(resource_path(shape_predictor_68_face_landmarks.dat))这段逻辑说明一下程序被打包后sys._MEIPASS 指向 PyInstaller 运行时解压出的临时目录模型文件会被释放到这里在源码运行时 sys._MEIPASS 不存在则退回当前目录。这个函数放在代码文件顶部所有文件读取路径都要走它包括后续可能加的配置文件。4.2 目标机器跑不起来的三大原因运行库、DLL 和权限把 exe 拷到一台干净的 Windows 机器上最常见的报错是找不到 vcruntime140.dll 或者 msvcp140.dll。这不是你的代码问题是目标机器缺少 VC 运行库。Python 解释器本身依赖这些 DLL但很多精简版 Windows 不会预装。两种解决方案一是把这两个 DLL 和 exe 放同一目录下发给用户简单粗暴二是用 -F 打包时PyInstaller 通常会尝试收集必要的 DLL但 dlib 这个库比较特殊它依赖的 DLL 比较多建议用工具打开 exe 检查一下依赖项。在打包完成后拿一台干净的虚拟机做冒烟测试是最稳妥的验证方式别用自己开发机测开发机上环境齐全什么都测不出来。另一类问题是 dlib 的线程库依赖。dlib 在 Windows 上编译时会链接 Intel TBBPyInstaller 打包后若缺 TBB DLL程序会在启动时静默崩溃或者报找不到 dll。遇到这类问题用 Process Explorer 或 Dependencies 工具查看 exe 加载失败的模块把缺失的 DLL 从 Python 的 site-packages 目录里复制到 exe 目录。这套排查流程虽然繁琐但本质是把 Python 环境里的动态库都聚齐到产物目录PyInstaller 的命令行参数解决不了的就手工补文件。4.3 Inno Setup 制作真正的安装向导exe 单独分发虽然能用但给使用者不够友好。要做成更像样的安装程序用 Inno Setup 写一个脚本把 exe、模型文件、VC 运行库安装包一起封装成 setup.exe。这里的价值在于安装程序会自动完成文件拷贝、桌面快捷方式创建、卸载信息写入注册表使用者拿到的是一个标准的 Windows 安装向导而不是一个疑似病毒的裸 exe。Inno Setup 的脚本文件不长一个最小可用的配置如下[Setup] AppNameDriver Fatigue Monitor AppVersion1.0 DefaultDirName{pf}\DriverFatigueMonitor OutputBaseFilenameDriverMonitor_Setup Compressionlzma2 SolidCompressionyes [Files] Source: dist\driver_monitor.exe; DestDir: {app} Source: shape_predictor_68_face_landmarks.dat; DestDir: {app} Source: vc_redist.x64.exe; DestDir: {app}; Flags: deleteafterinstall [Run] Filename: {app}\vc_redist.x64.exe; Parameters: /quiet /norestart; Flags: waituntilterminated Filename: {app}\driver_monitor.exe; Description: Launch application; Flags: postinstall nowait skipifsilent [Icons] Name: {group}\Driver Fatigue Monitor; Filename: {app}\driver_monitor.exe这段脚本做了三件事往安装目录写入 exe 和模型文件静默安装 VC 运行库这一步专门解决目标机器缺 DLL 的问题创建开始菜单快捷方式。在实际交付时我会把这段脚本配图和参数说明做成文档放在压缩包里用户拿到后点点下一步就能用。聊天中不少人问为什么这个安装报错、那个安装失败这类问题绝大多数是运行库缺失和权限不足两层原因把上面两步做进安装程序能省掉一大半现场排错。5. 现场最容易翻车的 5 个问题现象、原因与处理办法5.1 dlib 安装失败pip 报编译错误看不到任何有效的错误信息现象pip install dlib 执行到一半屏幕开始刷一堆 C 编译日志最后报 error: command cl.exe failed或者提示找不到 CMAKE。原因Windows 上 pip 会尝试从源码编译 dlib而编译需要 Visual Studio Build Tools 和 CMake 两样东西缺一不可。解决优先降级 Python 到 3.9 并指定装预编译包 dlib 19.24.0大多数情况下直接成功如果还是不行用 conda install -c conda-forge dlibconda 会帮你处理好工具链依赖。别在编译报错上死磕那是个无底洞。5.2 摄像头画面卡顿或打不开分辨率过高导致的帧率断崖现象程序能启动预览画面要么全黑要么过几秒就卡死控制台上打印的帧率不到 10。原因笔记本自带摄像头默认可能跑 1280x720 甚至更高再加上人脸检测的耗时CPU 算不过来。解决把 VideoCapture 的宽高强制设为 640x480并用 CAP_PROP_FPS 设为 30。如果还卡检查是不是同时开了微信、钉钉等多个占用摄像头的程序摄像头被占用时 OpenCV 拿到的帧全是黑画面。代码加三行cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)5.3 打哈欠误报率很高说话、喝水、叹气都被识别成哈欠现象正常聊天场景下系统频繁播报疲劳报警查看日志发现 MAR 过阈值次数很多。原因0.6 的瞬时阈值挡不住说话时的嘴部开合特别是大张嘴说话时 MAR 很容易瞬时超过 0.6。解决一是增加连续帧数约束MAR_THRESHOLD 连续保持 30 帧以上再判定哈欠二是采样用户正常说话的 MAR 曲线把阈值提升到采样均值的 1.5 倍以上。还有一个容易被忽略的点检测到人脸消失时比如司机转头看侧视镜关键点会短暂丢失此时要把计数器的累计状态冻结否则重新检测到人脸时计数器可能已经被旧数据占满造成误报。5.4 戴眼镜用户无法识别镜框反光和粗镜腿干扰关键点定位现象戴眼镜用户的 EAR 曲线抖动幅度明显偏大闭眼帧和睁眼帧区分不开。原因dlib 的 68 点回归模型对镜框边缘的强对比度敏感镜框反光会让眼睑关键点被吸到镜框上。解决在预处理阶段做一次自适应直方图均衡化再送入检测器代码是 cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8)).apply(gray)。对粗框眼镜效果改善明显对细框眼镜改善有限。如果测试中发现改善不理想只能在文档里注明推荐使用细框眼镜或者改用 MediaPipe 的 FaceMesh 模型作为关键点提取器它密集的 468 点对遮挡的容忍度要好一些但打包后体积更大需要权衡。5.5 打包后的 exe 在用户电脑上闪退没有任何提示事件查看器里才有记录现象exe 在自己机器上跑得挺好发给用户后双击没有反应或者闪一下黑窗口就消失。原因很多最常见的是缺 DLL 或模型文件路径不对。解决先用 -F 单文件模式打包再把一份不压缩目录文件放到同目录下测试看报错信息是否更明确。没有控制台窗口时先临时去掉 -w 参数打包一个带控制台的版本让用户在开发模式下运行一次错误信息会直接打出来。这类问题 80% 都落在缺运行库和模型文件没有通过 resource_path 加载上这两处检查完一般就能解决。最后建议维护一份常见问题清单交付给使用者记录“双击闪退就装运行库”“摄像头黑屏就关掉其他占用摄像头的软件”这两条能省掉大量电话沟通成本。6. 把固定阈值改成动态校准用 PERCLOS 思路把误报率降下来前三章的固定阈值方案在演示场景够用但真到连续跑一两个小时的实际环境里光照变化和用户姿态变化会让固定阈值的误报率暴露出来。工程上更稳的做法是引入启动校准和 PERCLOS 统计。启动校准的逻辑很简单程序启动后提示用户正视摄像头 1 到 2 秒采集这段视频里的 EAR 均值作为 baseline然后取 baseline 的 75% 作为闭眼阈值。这样不同眼睛大小、是否戴眼镜的用户都能自动适配阈值。单次眨眼判定也做一个人均化处理把用户正常的眨眼时长采样出来超过正常值 3 倍以上的单次闭眼才判定为瞌睡级闭眼。疲劳判定用 PERCLOS 滑动窗口替代瞬时计数。PERCLOS 是眼睑闭合时间比例的行业标准算法统计方式是每秒计算一次“在过去 30 秒内闭眼帧数占总帧数的比例”超过 40% 判定为疲劳。这和单个眨眼计数完全不同眨眼频率高但每次闭眼时间短的人PERCLOS 不会触发疲劳而闭眼时间偏长但频率不高的人PERCLOS 会及时报警。实现上只需维护一个保存最近 900 帧眼睛状态的环形队列每帧推入一个布尔值弹出最旧的一个再统计队列里 True 的占比。这个方案比固定阈值鲁棒得多也是我目前交付版本里默认启用的模式。验证方法上别只在实时摄像头下测。我一般会录制两段视频一段 3 分钟正常驾驶状态一段 3 分钟模拟疲劳状态频率眨眼、打哈欠、点头离线跑完统计误报率和漏报率。通过调节 PERCLOS 的窗口长度和 40% 的疲劳线可以在误报和漏报之间找到平衡点。输出一份带时间戳的报警日志方便判断哪些报警是误报。这套检测方案说到底是个概率系统没有任何阈值能零误报把它想成驾驶辅助而不是替人开车定位就清晰了。我给自己定的习惯是每次交付都附上报警日志说明用户跑几周后把日志发回来再用真实数据微调参数迭代两轮后可靠性会好很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表