资讯详情

Python驾驶员面部特征疲劳检测系统:从EAR/MAR到头部姿态的完整实现与调参避坑指南

📅 2026/10/10 14:44:04 | 华诺云谱 👁 阅读
Python驾驶员面部特征疲劳检测系统:从EAR/MAR到头部姿态的完整实现与调参避坑指南
简介这份资源是一套基于Python开发的驾驶员面部特征疲劳检测系统源码面向计算机相关专业学生、毕业设计选题者以及需要人脸识别与状态监测方案的开发者。项目通过摄像头采集驾驶员面部信息识别其疲劳状态并判断是否需要休息可迁移至交警监控、高速收费站等场景用于排查疲劳驾驶行为。压缩包共24个文件约68.33MB包含5个py核心脚本、10个xml配置与界面文件、iml与gitignore等工程配置、md说明文档、docx文档、dat数据文件及mp3提示音等覆盖从模型调用到界面交互的完整流程。目前已有1358人学习下载说明该方案在同类选题中具备一定参考价值。源码包下载后可直接运行目录结构清晰便于读者快速理解人脸检测、特征提取与疲劳判定的实现思路并在此基础上进行功能扩展或二次开发适合作为毕业设计原型或课程实践项目。1. 从一张摄像头截图说起疲劳检测到底在检测什么你打开电脑摄像头画面里是一张普通的人脸。系统要在几百毫秒内判断这个人是不是困了。这不是玄学而是把面部特征转成可量化的数字——眼睛睁多大、嘴巴张多开、头低了多少度——再和阈值比对。标题里的“Python基于驾驶员面部特征的疲劳检测系统源码”本质就是一套用 Python 把摄像头画面变成疲劳分数的流水线。它适合做毕业设计的学生、想入门计算机视觉的开发者以及需要给车载或工位场景加一层轻量预警的工程师。核心链路只有四步人脸检测、关键点定位、特征计算、疲劳判定。每一步都有现成库但参数和阈值才是决定这套系统能不能用的关键。下面我把这条链路拆开从环境搭到调参再到踩过的坑全部讲清楚。2. 环境搭建与依赖选型为什么是 OpenCV dlib 而不是别的2.1 三个库的分工与版本选择这套系统最稳的组合是 OpenCV 负责读摄像头和画框dlib 负责 68 点人脸关键点NumPy 负责算距离和比例。不推荐用 MediaPipe 替代 dlib 做毕业设计原因有两个一是 dlib 的 68 点模型输出稳定论文里好写公式二是 MediaPipe 的 API 变动频繁你照着半年前的教程跑很可能报错。Python 版本选 3.8 到 3.10 之间3.11 以上 dlib 的预编译包不好找自己编译要装 CMake 和 Visual Studio Build Tools对新手不友好。安装顺序很重要先装 NumPy 再装 OpenCV最后装 dlib。dlib 在 Windows 上直接 pip install dlib 大概率失败常见做法是下载对应的 .whl 文件本地安装。下面是一段可复现的安装命令假设你用的是 Windows Python 3.9。# 先升级 pip避免旧版解析 wheel 出错 python -m pip install --upgrade pip # 安装基础库指定版本避免兼容问题 pip install numpy1.23.5 pip install opencv-python4.7.0.72 # dlib 不要直接 pip install dlib先下 whl # 去 dlib 官方或可信镜像找 dlib-19.24.0-cp39-cp39-win_amd64.whl pip install dlib-19.24.0-cp39-cp39-win_amd64.whl # 验证三个库都能导入 python -c import cv2, dlib, numpy; print(cv2.__version__, dlib.__version__, numpy.__version__)逻辑说明NumPy 是 dlib 和 OpenCV 的底层依赖先装它能让后续库直接复用。OpenCV 用 4.7 版本是因为它的 VideoCapture 在 Windows 上对多数摄像头兼容性最好。dlib 用 whl 安装绕开编译省去装 Visual Studio 的麻烦。参数上numpy 1.23.5 和 opencv-python 4.7.0.72 是经过验证不冲突的组合你如果装最新版可能会遇到 dlib 导入时报 DLL 缺失。2.2 摄像头初始化与分辨率设置很多教程直接 cv2.VideoCapture(0) 就完事但实际跑起来会发现帧率不稳、画面延迟。我一般会显式设置分辨率和缓冲区大小。分辨率不是越高越好640x480 足够做面部特征检测再高只会拖慢帧率。下面这段代码是摄像头初始化的最小可用版本。import cv2 # 打开默认摄像头0 表示第一个设备 cap cv2.VideoCapture(0) # 设置分辨率640x480 是疲劳检测的甜点值 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 减少缓冲区降低画面延迟 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 检查是否成功打开 if not cap.isOpened(): raise RuntimeError(摄像头打开失败检查是否被其他程序占用) while True: ret, frame cap.read() if not ret: break # 水平翻转符合照镜子习惯 frame cv2.flip(frame, 1) cv2.imshow(frame, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明CAP_PROP_BUFFERSIZE 设为 1 是关键默认缓冲区可能缓存好几帧导致你看到的画面比实际慢半秒。翻转画面是为了让用户操作更自然不影响检测结果。参数上640x480 下 dlib 的 68 点检测单帧大约 30 到 50 毫秒普通 CPU 就能跑到 15 帧以上满足实时性。如果你用 1280x720检测时间会翻倍帧率掉到 8 帧以下疲劳判定就会滞后。2.3 dlib 关键点模型加载与首次运行dlib 需要单独下载 shape_predictor_68_face_landmarks.dat 模型文件大约 100MB。这个文件不在 pip 包里要去 dlib 官网下载。加载模型只需要一行但首次加载会花一两秒建议放在循环外面。import dlib # 初始化人脸检测器和关键点预测器 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) # 在灰度图上检测人脸 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) # 第二个参数 0 表示不放大图像 for face in faces: landmarks predictor(gray, face) # landmarks.part(i) 返回第 i 个点的坐标 print(landmarks.part(36).x, landmarks.part(36).y)逻辑说明detector 的第二个参数是上采样次数设 0 表示原图检测速度快但小脸可能漏检设 1 会放大一倍检测更稳但耗时增加。毕业设计场景下人脸通常占画面比例较大用 0 就够。landmarks 的 68 个点里36 到 41 是左眼42 到 47 是右眼48 到 67 是嘴巴这些索引后面算 EAR 和 MAR 要用到。3. 面部特征计算EAR、MAR 和头部姿态的代码实现3.1 眼睛纵横比 EAR 的公式与代码EAR 是 Eye Aspect Ratio 的缩写思路很简单眼睛睁大时上下眼睑距离大闭眼时距离趋近于零。用六个眼睛关键点算一个比值就能量化睁闭状态。公式是上眼睑到下眼睑的距离之和除以左右眼角距离的两倍。下面是对左右眼分别计算再取平均的代码。import numpy as np from scipy.spatial import distance as dist def eye_aspect_ratio(eye_points): # eye_points 是 6 个 (x, y) 坐标顺序为左角、上左、上右、右角、下右、下左 # 垂直距离上左到上右下左到下右 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 公式乘以 2.0 是为了归一化 ear (A B) / (2.0 * C) return ear # 从 landmarks 提取左右眼坐标 left_eye [] right_eye [] for i in range(36, 42): left_eye.append((landmarks.part(i).x, landmarks.part(i).y)) for i in range(42, 48): right_eye.append((landmarks.part(i).x, landmarks.part(i).y)) left_ear eye_aspect_ratio(left_eye) right_ear eye_aspect_ratio(right_eye) ear (left_ear right_ear) / 2.0逻辑说明scipy 的 euclidean 算两点距离比手写平方根更稳。EAR 的正常睁眼值在 0.25 到 0.35 之间闭眼会降到 0.15 以下。注意左右眼要分别算再平均因为侧脸时一只眼可能被遮挡单眼 EAR 会失真。参数上如果发现 EAR 整体偏低检查关键点索引有没有错位36 到 41 是左眼还是右眼取决于 dlib 的定义实际跑一遍打印坐标就能确认。3.2 嘴巴纵横比 MAR 与打哈欠判定MAR 是 Mouth Aspect Ratio和 EAR 思路一样用嘴巴的垂直距离除以水平距离。打哈欠时嘴巴张开MAR 会明显升高。代码结构和 EAR 几乎一样只是关键点索引换成 48 到 67。def mouth_aspect_ratio(mouth_points): # 取上下唇中间的几个点算垂直距离 # 48 是左嘴角54 是右嘴角51 是上唇中点57 是下唇中点 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 [] for i in range(48, 68): mouth.append((landmarks.part(i).x, landmarks.part(i).y)) mar mouth_aspect_ratio(mouth)逻辑说明这里用的索引是 48 到 67 里的相对位置mouth_points[0] 对应 48mouth_points[6] 对应 54。垂直距离取了两组是为了减少单点抖动。MAR 正常闭嘴在 0.1 到 0.3打哈欠能到 0.6 以上。参数上如果 MAR 一直偏高可能是嘴巴关键点把下巴也算进去了检查 48 到 67 的坐标分布正常应该集中在嘴唇周围。3.3 头部姿态估计用 solvePnP 算俯仰角疲劳时人往往会低头所以头部俯仰角是一个重要特征。OpenCV 的 solvePnP 可以用 2D 关键点和 3D 人脸模型算旋转向量再转成欧拉角。3D 模型点我一般用六个稳定点鼻尖、下巴、左右眼角、左右嘴角。# 3D 人脸模型点单位任意比例对就行 model_points np.array([ (0.0, 0.0, 0.0), # 鼻尖 (0.0, -63.6, -12.5), # 下巴 (-43.3, 32.7, -26.0), # 左眼角 (43.3, 32.7, -26.0), # 右眼角 (-28.9, -28.9, -24.1), # 左嘴角 (28.9, -28.9, -24.1) # 右嘴角 ], dtypenp.float64) # 对应的 2D 关键点索引 image_points np.array([ (landmarks.part(30).x, landmarks.part(30).y), # 鼻尖 (landmarks.part(8).x, landmarks.part(8).y), # 下巴 (landmarks.part(36).x, landmarks.part(36).y), # 左眼角 (landmarks.part(45).x, landmarks.part(45).y), # 右眼角 (landmarks.part(48).x, landmarks.part(48).y), # 左嘴角 (landmarks.part(54).x, landmarks.part(54).y) # 右嘴角 ], dtypenp.float64) # 相机内参用画面宽高估算 focal_length frame.shape[1] center (frame.shape[1] / 2, frame.shape[0] / 2) camera_matrix np.array([ [focal_length, 0, center[0]], [0, focal_length, center[1]], [0, 0, 1] ], dtypenp.float64) # 假设没有镜头畸变 dist_coeffs np.zeros((4, 1)) success, rotation_vector, translation_vector cv2.solvePnP( model_points, image_points, camera_matrix, dist_coeffs, flagscv2.SOLVEPNP_ITERATIVE ) # 旋转向量转欧拉角 rotation_matrix, _ cv2.Rodrigues(rotation_vector) # 这里只取俯仰角即绕 X 轴的旋转 pitch np.degrees(np.arcsin(-rotation_matrix[2][1]))逻辑说明solvePnP 需要至少四个点我用六个是为了更稳。相机内参用焦距等于画面宽度是粗略估计毕业设计够用精确标定要另做。pitch 为负表示低头正常坐姿在 -10 到 10 度之间低于 -20 度持续几秒就可以算疲劳特征。参数上如果 pitch 跳变厉害检查 image_points 的顺序和 model_points 是否一一对应顺序错一个点结果就全乱。4. 疲劳判定逻辑与阈值调参从单帧到时间窗口4.1 用连续帧计数器替代单帧阈值单帧 EAR 低于阈值不能直接判定疲劳因为眨眼也会让 EAR 瞬间降低。常见做法是设一个计数器连续 N 帧 EAR 都低于阈值才触发报警。N 的取值和帧率有关15 帧下 N 取 3 到 5 比较合适对应 0.2 到 0.3 秒。下面是一个可调参数的判定逻辑。# 可调参数 EAR_THRESHOLD 0.21 # 闭眼阈值 EAR_CONSEC_FRAMES 3 # 连续帧数 MAR_THRESHOLD 0.6 # 打哈欠阈值 PITCH_THRESHOLD -20 # 低头角度阈值 eye_counter 0 yawn_counter 0 fatigue_score 0 if ear EAR_THRESHOLD: eye_counter 1 if eye_counter EAR_CONSEC_FRAMES: fatigue_score 1 else: eye_counter 0 if mar MAR_THRESHOLD: yawn_counter 1 if yawn_counter 5: fatigue_score 1 else: yawn_counter 0 if pitch PITCH_THRESHOLD: fatigue_score 1 # 疲劳分数超过阈值就报警 if fatigue_score 3: cv2.putText(frame, FATIGUE ALERT, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) fatigue_score 0 # 报警后重置避免持续刷屏逻辑说明eye_counter 在 EAR 恢复正常时清零保证只有持续闭眼才计数。fatigue_score 是累加分数不同特征各占一分超过 3 分报警。参数上EAR_THRESHOLD 是最关键的不同人眼型差异大戴眼镜的人 EAR 整体偏低建议先用默认值跑一遍打印每个人的 EAR 分布再微调。MAR_THRESHOLD 对打哈欠的判定比较宽松因为打哈欠持续时间长不容易误判。4.2 阈值标定用一段视频跑出个人基线没有一套阈值适合所有人。我一般会让使用者先正常睁眼坐 10 秒记录 EAR 均值再闭眼 3 秒记录最低值取中间值作为阈值。下面这段代码可以帮你采集基线。import time # 采集 10 秒正常状态的 EAR baseline_ears [] start time.time() while time.time() - start 10: ret, frame cap.read() gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) for face in faces: landmarks predictor(gray, face) # ... 计算 ear ... baseline_ears.append(ear) cv2.imshow(calibrate, frame) if cv2.waitKey(1) 0xFF ord(q): break mean_ear np.mean(baseline_ears) std_ear np.std(baseline_ears) # 阈值设为均值减去 1.5 倍标准差 EAR_THRESHOLD mean_ear - 1.5 * std_ear print(f基线 EAR: {mean_ear:.3f}, 建议阈值: {EAR_THRESHOLD:.3f})逻辑说明用均值减 1.5 倍标准差是统计上的常用做法能覆盖大部分正常波动。如果标准差很大说明采集时头部晃动多建议重新采集。参数上采集时间 10 秒是经验值太短不稳定太长用户不耐烦。标定完把阈值写进配置文件下次直接读不用每次重跑。4.3 多特征融合的权重分配EAR、MAR、pitch 三个特征对疲劳的贡献不一样。闭眼是最直接的信号权重可以给 0.5打哈欠给 0.3低头给 0.2。加权求和后和阈值比较比简单累加更合理。特征权重正常范围疲劳触发条件EAR0.50.25-0.35连续 3 帧低于 0.21MAR0.30.1-0.3连续 5 帧高于 0.6Pitch0.2-10 到 10低于 -20 度持续 2 秒逻辑说明权重可以根据场景调比如夜间行车闭眼更危险EAR 权重可以提到 0.6。表格里的正常范围是统计经验值实际用的时候以个人基线为准。注意 pitch 的触发条件加了持续时间因为低头一瞬间可能是看仪表盘不一定是疲劳。5. 避坑与排查那些让我熬夜的翻车现场5.1 摄像头被占用导致 cap.read() 返回 False现象程序跑起来第一帧就退出cap.isOpened() 返回 True 但 read() 一直 False。原因摄像头被其他程序占用比如微信视频、Zoom 或者另一个 Python 进程没释放。解决关掉所有可能用摄像头的软件在任务管理器里结束残留的 python.exe 进程。如果还不行换一个 USB 接口有些笔记本的内置摄像头和某些外接设备冲突。5.2 dlib 关键点抖动导致 EAR 跳变现象人坐着不动EAR 却在 0.2 到 0.35 之间来回跳计数器频繁触发。原因dlib 的 68 点检测本身有像素级抖动尤其是光线不足时。解决对 EAR 做滑动平均取最近 5 帧的均值再判定。代码上用一个 deque 存历史值每次算完 append 再求平均。另外保证面部光照均匀背光会让关键点漂移。5.3 戴眼镜时 EAR 阈值失效现象戴眼镜的人正常睁眼 EAR 只有 0.18用默认阈值 0.21 会一直报警。原因镜片反光和镜框遮挡让眼睛关键点偏移上下眼睑距离被压缩。解决让戴眼镜的用户单独标定基线或者把 EAR_THRESHOLD 降到 0.15。更稳的做法是检测眼镜区域但毕业设计没必要标定就能解决。5.4 多线程读取摄像头导致帧率不稳现象加了报警声音播放后画面卡顿EAR 计算滞后。原因声音播放是阻塞操作占用了主循环时间。解决把声音播放放到独立线程用 threading 模块。主循环只负责读帧和计算报警时往队列里丢一个信号声音线程从队列取。这样主循环帧率不受影响。5.5 打包成 exe 后模型文件找不到现象PyCharm 里跑得好好的用 PyInstaller 打包后报错找不到 shape_predictor_68_face_landmarks.dat。原因打包时模型文件没有一起打进去或者路径用了相对路径。解决用 --add-data 参数把 dat 文件加进去代码里用 sys._MEIPASS 获取临时目录路径。下面是一段兼容开发和打包的路径处理。import sys import os def resource_path(relative_path): # 打包后 sys._MEIPASS 是临时解压目录 if hasattr(sys, _MEIPASS): return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.abspath(.), relative_path) predictor dlib.shape_predictor(resource_path(shape_predictor_68_face_landmarks.dat))逻辑说明sys._MEIPASS 是 PyInstaller 运行时创建的临时目录打包时用 --add-data shape_predictor_68_face_landmarks.dat;. 把文件放进去。参数上分号前面是源文件后面是目标目录Windows 用分号Linux 用冒号。6. 进阶技巧用 EAR 历史曲线做趋势预警前面讲的都是瞬时判定但疲劳是一个累积过程。我后来加了一个趋势预警把最近 30 秒的 EAR 画成曲线如果曲线整体下移且波动变小说明人在进入微睡眠状态这时候即使还没触发阈值也可以提前提醒。实现上用一个固定长度的 deque 存 EAR每帧更新用 matplotlib 或者 OpenCV 的折线画在画面角落。from collections import deque # 存最近 30 秒的 EAR假设 15 帧每秒 ear_history deque(maxlen450) # 每帧计算完 ear 后 ear_history.append(ear) # 计算趋势最近 5 秒均值和前 5 秒均值比较 if len(ear_history) 150: recent np.mean(list(ear_history)[-75:]) previous np.mean(list(ear_history)[-150:-75]) if recent previous * 0.85: cv2.putText(frame, DROWSINESS TREND, (50, 100), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 165, 255), 2)逻辑说明deque 的 maxlen 自动丢弃旧数据不用手动清理。recent 和 previous 各取 75 帧对应 5 秒。比值 0.85 是经验值表示 EAR 下降了 15% 以上。这个趋势预警比瞬时阈值早 2 到 3 秒发现疲劳给驾驶员留出反应时间。参数上如果你的帧率不是 15按实际帧率调整窗口大小公式是 帧率乘以秒数。验证这套系统好不好用我一般会做两个测试一是正常看视频 10 分钟看误报次数二是模拟疲劳闭眼 3 秒、打哈欠 5 次、低头 10 秒看能不能全部触发。误报超过 3 次就调高阈值漏报就调低。最后说一个血泪经验不要用网上的默认阈值直接交毕业设计答辩老师让你现场演示换个人脸可能就翻车。花 10 分钟做个人基线标定比调一天参数都管用。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑