十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于面部特征的疲劳检测系统:Python实现与工程落地指南

基于面部特征的疲劳检测系统:Python实现与工程落地指南 简介这套Python源码实现了一套基于驾驶员面部特征的疲劳检测系统面向计算机视觉、交通监控及智能驾驶相关学习者与开发者。系统通过摄像头实时采集面部图像利用OpenCV等工具定位人脸与眼睛区域分析眨眼频率、眼睛闭合时间等关键指标并在疲劳特征超过阈值时触发音频预警内置面部检测、眼睛定位、疲劳分析、预警机制及用户界面等模块整体结构清晰易于扩展。资源共22个文件以5个Python脚本为核心辅以10个XML配置文件用于级联分类器与项目配置、1个MP3预警提示音、1个dat模型/数据文件以及README说明文档等压缩包整体约68.32MB目录结构清晰便于按模块阅读理解或二次开发。目前已有92人学习下载适合希望掌握疲劳检测完整流程、熟悉面部关键点采集与特征判断逻辑的读者也可作为毕业设计或课题实验的参考基础。1. 基于驾驶员面部特征的疲劳检测系统核心是把“困不困”计算成几何距离和时间占比网上流传的 Python 疲劳检测开源项目多数是一个课程设计或毕设形态的工程包包含人脸检测、关键点提取、闭眼判断、哈欠检测和告警控制。真正去读源码时大部分人卡住的地方不是装不上依赖而是看不懂为什么几个坐标点之间的比值就能代表疲劳。这套系统的本质是把“困不困”翻译成三个可连续计算的物理量眼睛纵横比反映眼睑开合程度闭眼时间占比反映一段时间内的困倦累积嘴部纵横比辅助识别哈欠。接下来先把每个特征的几何来源讲清楚再给出一套能直接跑起来的 Python 实现骨架最后落到参数标定和离线验证这些最容易踩坑的环节。内容适合正在做二次开发、复现论文或者给课程设计加功能的工程师。2. 疲劳信号怎么量化EAR、PERCLOS 与 MAR 在 Python 里的计算逻辑要让程序判断疲劳必须先定义什么叫“眼睛闭上了”。直接用深度学习分类器去识别困倦表情的人不在少数但疲劳检测更需要的是连续、低延迟、可解释的度量所以几何特征在这个场景里优势明显计算量小、可调参、能回溯每一帧的判定依据。这一章把系统里最核心的三个特征讲透后面的代码全部建立在这三个量之上。2.1 眼部纵横比 EAR用眼皮关键点距离替代睁眼分类dlib 的 68 点人脸模型中每只眼睛由六个关键点标定内眼角、外眼角、上眼睑两个点、下眼睑两个点。用这些点的坐标算一个“眼睑张开程度”的比例就是 EAREye Aspect Ratio。耳熟能详的公式是垂直方向两段距离的均值除以水平方向距离正常睁眼时比值大约在 0.25 到 0.35闭眼时会落到 0.15 以下不需要训练模型一个线性阈值就能区分睁眼和闭眼。import numpy as np LEFT_EYE [33, 160, 158, 133, 153, 144] # MediaPipe FaceMesh 左眼 6 点索引 RIGHT_EYE [362, 385, 387, 263, 373, 380] # 右眼对应索引 def ear(landmarks, eye_pts): 计算单眼纵横比。landmarks 是 FaceMesh 返回的归一化坐标列表。 pts np.array([(landmarks[i].x, landmarks[i].y) for i in eye_pts]) # 垂直方向取左右两侧距离做平均抵消头部轻微偏转造成的误差 v1 np.linalg.norm(pts[1] - pts[5]) v2 np.linalg.norm(pts[2] - pts[4]) h np.linalg.norm(pts[0] - pts[3]) return (v1 v2) / (2.0 * h)代码里的索引顺序不能随便调换。pts[1]到pts[5]必须构成一对上下对应的眼睑采样点pts[2]到pts[4]是另一对pts[0]和pts[3]是内眼角和外眼角。如果从源码里看到不同的索引数组只要保持“六点按顺时针或逆时针连续排列”即可套用同一个函数。把左右眼 EAR 再做一次平均能进一步压低单眼遮挡或反光造成的抖动。2.2 PERCLOS 统计用滑动窗口闭眼占比代替单帧判决单帧 EAR 掉到阈值以下不代表疲劳正常眨眼也会让 EAR 短暂下探。疲劳检测系统需要的是“一段时间里闭眼时间占比”这正是从驾驶疲劳研究中沿用下来的 PERCLOSPercentage of Eye Closure指标。工程上最常用 P80 规则眼睑遮住瞳孔面积超过 80% 就算闭眼统计一段时间内闭眼帧数占总帧数的比例。这个比例直接对应反应迟钝和注意力涣散的程度。from collections import deque PERCLOS_WINDOW 60 # 统计窗口长度按 30 fps 即 2 秒 EYE_CLOSE_EAR 0.21 # 低于该 EAR 视为闭眼 PERCLOS_ALARM 0.35 # 闭眼时间占比超过 35% 触发告警 win deque(maxlenPERCLOS_WINDOW) def push_ear(cur_ear): 将当前帧 EAR 压入滑窗返回窗口内闭眼占比。 closed 1 if cur_ear EYE_CLOSE_EAR else 0 win.append(closed) if len(win) PERCLOS_WINDOW: return 0.0 return sum(win) / len(win)使用deque(maxlen...)的好处是每帧入队时自动淘汰最老数据sum(win)直接得到闭眼总帧数整个统计过程不依赖外部计数器。窗口长度按帧率换算30 fps 下 60 帧代表 2 秒50 fps 下 60 帧只代表 1.2 秒同样的告警占比语义完全不同所以从文件读源码时第一件事是去看fps是怎么算的。若项目里直接用“连续闭眼帧数”判断疲劳本质上也是 PERCLOS 的简化版本只是没有考虑窗口内的睁眼中断。2.3 嘴部纵横比 MAR 与头部下沉趋势补上哈欠和点头两个维度单靠眼睛特征有一个盲区司机在进入明显闭眼状态之前会先经历频繁打哈欠和头部不自觉下沉。多数源码会在 EAR 之外加上嘴部纵横比 MAR公式与 EAR 同构只是采样点换成嘴唇外轮廓上唇中点与下唇中点的垂直距离除以左右嘴角的水平距离。打哈欠时 MAR 会从通常的 0.2 附近跳到 0.6 以上。MOUTH_TOP, MOUTH_BOTTOM 13, 14 MOUTH_LEFT, MOUTH_RIGHT 61, 291 def mar(landmarks): 计算嘴部纵横比用于哈欠检测。 top np.array([landmarks[MOUTH_TOP].x, landmarks[MOUTH_TOP].y]) bottom np.array([landmarks[MOUTH_BOTTOM].x, landmarks[MOUTH_BOTTOM].y]) left np.array([landmarks[MOUTH_LEFT].x, landmarks[MOUTH_LEFT].y]) right np.array([landmarks[MOUTH_RIGHT].x, landmarks[MOUTH_RIGHT].y]) height np.linalg.norm(top - bottom) width np.linalg.norm(left - right) return height / width这个函数的返回值在正常表情和哈欠之间有明显的台阶式差异适合用状态机统计次数MAR 连续 N 帧大于阈值记为一次哈欠回落后计数器加一。头部姿态在大部分源码里不需要专门做 3D 姿态估计直接观察鼻尖与两眼中心连线的纵向距离变化即可判断头部是否逐渐下沉只有当项目需要输出俯仰角给仪表盘时才引入solvePnP做六自由度姿态解算。3. Python 疲劳检测系统实现关键点提取、主循环与告警状态机的完整骨架理论是三件套落到代码就要处理视频帧循环、断帧、多人脸和告警去抖。这一章给出一套可以直接跑通的 Python 实现骨架并逐块解释为什么这么写。改造别人源码时照着这个结构去对比差异比通读全部文件要快得多。3.1 关键点提取选型MediaPipe FaceMesh 比 dlib 更适合源码改造大量旧版源码包默认搭配 dlib 的shape_predictor_68_face_landmarks.dat这个模型稳定但有三个麻烦模型文件需单独下载dlib 编译依赖 CMake 和 C 工具链侧脸大于 45° 时关键点容易飞掉。改造时建议换用 MediaPipe FaceMesh它自带人脸检测与关键点回归pip install mediapipe即装即用返回 468 个关键点且带三维坐标眼部、嘴部轮廓采样更密。对比维度如下对比项dlib 68 点MediaPipe FaceMesh安装方式源码编译额外下载 .dat 模型pip 安装模型内嵌关键点数量68468含 3D 坐标侧脸鲁棒性一般大于 45° 容易丢点较好对头部转动容忍度更高CPU 单帧耗时约 10-30 ms约 20-40 ms受分辨率影响二次开发需要自己维护人脸检测器检测与跟踪一体化返回对“基于驾驶员面部特征的疲劳检测系统”这类源码FaceMesh 的虹膜关键点还能为后续扩展视线方向判断做铺垫改造成本远低于 dlib 换模型文件带来的连锁问题。3.2 检测主循环取帧、提取关键点、计算疲劳指标import cv2 import mediapipe as mp mp_face_mesh mp.solutions.face_mesh face_mesh mp_face_mesh.FaceMesh( static_image_modeFalse, max_num_faces1, refine_landmarksTrue, # 启用虹膜关键点方便做视线扩展 min_detection_confidence0.5, min_tracking_confidence0.5, ) cap cv2.VideoCapture(0) fps cap.get(cv2.CAP_PROP_FPS) or 30 while cap.isOpened(): ret, frame cap.read() if not ret: continue # MediaPipe 的 process 接口接收 RGBOpenCV 默认 BGR rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results face_mesh.process(rgb) if results.multi_face_landmarks: lm results.multi_face_landmarks[0].landmark # 左右眼 EAR 取平均单眼失效时仍能维持稳定输出 cur_ear (ear(lm, LEFT_EYE) ear(lm, RIGHT_EYE)) / 2.0 cur_mar mar(lm) closed_ratio push_ear(cur_ear) if closed_ratio PERCLOS_ALARM: cv2.putText(frame, FATIGUE WARNING, (30, 60), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3) else: win.clear() # 人脸丢失时清空滑窗避免脏数据带入下一段 cv2.imshow(Driver Fatigue Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段循环里有四个关键点需要注意。第一颜色通道必须先由 BGR 转 RGB漏掉这一步会出现检测结果时有时无的现象这是替换源码时最常见的低级错误。第二max_num_faces1限制只追踪一个人脸车内副驾或后排人脸不会被误识别如果项目需要同时检测司机和乘客改成 2 后要按人脸框面积或距画面中心距离选择主目标。第三closed_ratio只在窗口填满后才返回非零值因此前 2 秒不会触发误报这是刻意设计。第四人脸丢失时调用win.clear()否则重新入画后会把旧数据带入新窗口导致 PERCLOS 虚高。3.3 告警状态机用两级计数器过滤眨眼和短暂低头直接用closed_ratio PERCLOS_ALARM触发告警会产生体验问题司机看后视镜、揉眼睛、被强光晃到的一瞬间都可能让指标短暂超限。多数工程方案在统计之外再加两级计数器第一级过滤正常眨眼第二级累积“疑似疲劳”证据用上升快、下降慢的方式避免告警频繁闪烁。BLINK_FRAMES int(fps * 0.4) # 持续 0.4 秒以上才不算正常眨眼 close_counter 0 suspicious_counter 0 def update_state(cur_ear): global close_counter, suspicious_counter if cur_ear EYE_CLOSE_EAR: close_counter 1 else: close_counter 0 if close_counter BLINK_FRAMES: suspicious_counter 1 else: suspicious_counter max(0, suspicious_counter - 2) return suspicious_counter int(fps * 3) # 疑似证据持续 3 秒才告警close_counter的作用是区分“闭眼”与“眨眼”0.4 秒按 30 fps 折算成 12 帧低于这个帧数的 EAR 下探直接被忽略。suspicious_counter每次加 1、回退减 2意味着一两次短暂闭眼不会累积到告警而持续性的闭眼行为会迅速堆高计数值。这两个计数器配合 PERCLOS 滑窗能同时覆盖“高频短闭眼”和“低频长闭眼”两种疲劳表现。3.4 疲劳检测系统的初始参数表默认值只是起点参数推荐初值调整方向EYE_CLOSE_EAR0.21调大则判定变严容易把微睁眼判为闭眼调小则漏检增加MAR 哈欠阈值0.55嘴型差异大按实际测试人员标定PERCLOS 窗口60 帧帧率不同要换算保证时间窗口在 2 秒左右PERCLOS 告警线0.35对疲劳敏感度要求高的场景可下调至 0.25最短闭眼帧数0.4 秒 × fps过滤正常眨眼不宜小于 0.2 秒这些初值来自大多数开源项目的默认配置但绝不能当作最终参数。驾驶员个体差异、摄像头安装角度、车内光照都会显著改变 EAR 的绝对值下一章讲的是拿到源码后怎么标定和验证而不是直接上线。4. 现场排错与参数标定疲劳检测系统在真实驾驶场景里的 4 个关键调整代码能跑通与系统能用是两回事。疲劳检测系统在车内遇到的干扰远多于实验室遮阳板阴影、眼镜反光、夜间红外补光都会直接改变 EAR 和 MAR 的数值。这一章把重复出现的问题聚成四类每一类都给出具体处理手段。4.1 低照度与逆光先对画面做亮度归一化再进检测器当画面光线不足或逆光时MediaPipe 依然返回关键点坐标但这些坐标的置信度很低EAR 会出现高频抖动。处理办法是在取帧后对亮度通道做直方图均衡化增强眼部区域的对比度。def preprocess_frame(frame): 提亮 对比度增强仅用于低照度条件。 yuv cv2.cvtColor(frame, cv2.COLOR_BGR2YUV) yuv[:, :, 0] cv2.equalizeHist(yuv[:, :, 0]) return cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR)直方图均衡化对夜间或侧光照下的关键点召回率提升立竿见影但正常日光下会引入额外噪点。实际部署时应先统计最近 30 帧的平均亮度低于设定阈值再启用预处理而不是每帧都跑。另一个针对性处理是判断results.multi_face_landmarks的置信度当检测置信度低于 0.5 时直接沿用上一帧的 EAR 值避免把低质量检测结果带入统计窗口。4.2 摄像头安装距离与角度EAR 基线随距离漂移的换算EAR 本质上是坐标比值理论上不受分辨率影响但实际受摄像头距离影响显著。同一个人的同一只眼在 0.5 米处眼裂水平距离可能占 200 像素到 1.2 米处只剩 60 像素像素量化噪声会让 EAR 从 0.28 抖到 0.24。安装时应当保证人脸宽度占画面宽度的四分之一到二分之一摄像头正对驾驶员面部侧向夹角尽量小于 35°。如果摄像头位置已经固定可以在系统启动后录制 5 秒正常睁眼画面计算该司机的 EAR 均值作为基线再用“基线 − 0.08”作为闭环阈值。这种自适应标定比写死某个值更符合实际也是多数量产方案的做法。需要留意的是不要把基线直接作为告警阈值否则正常眨眼就可能触发。4.3 用人工标注视频评估误报与漏检调阈值前先做量化很多源码包在 README 里写了“阈值可根据需要调整”但没说怎么调。正确的做法是准备一段包含正常驾驶、打哈欠、闭眼疲劳三种状态的视频逐帧或者按时间段标注真值然后对比程序输出计算召回率和误报率。下面给出用 numpy 评估一个阈值好坏的代码骨架def eval_threshold(ear_log, label, thr): ear_log: 每帧的 EAR 值列表label: 0 睁眼 / 1 闭眼。 pred [1 if e thr else 0 for e in ear_log] tp sum(p 1 and l 1 for p, l in zip(pred, label)) fn sum(p 0 and l 1 for p, l in zip(pred, label)) fp sum(p 1 and l 0 for p, l in zip(pred, label)) recall tp / (tp fn) if tp fn else 0 fpr fp / (len(label) - (tp fn)) if tp fn ! len(label) else 0 return recall, fpr评估逻辑并不复杂tp是正确识别出的闭眼帧数fn是漏掉的闭眼帧数fp是把睁眼误判为闭眼的帧数。在 0.15 到 0.30 之间按步长 0.01 扫描阈值绘制召回率与误报率的曲线选择误报率小于 5% 且召回率最高的点作为最终值。若 recall 高但 fpr 高说明闭眼判决太宽松应当调大EYE_CLOSE_EAR反之则调小。这个流程每次改完参数都可以自动跑一遍不会出现“感觉效果好了一些”这种无法量化的状态。潜台词EAR 只反映“眼睛开合比例”但每个人的眼型深浅不同。眼窝深、眼裂长的司机 EAR 基线天然偏高短眼裂司机的闭眼 EAR 可能只有 0.12所以跨人用同一阈值必然有问题。量化的价值就在这里它让你能区分“模型问题”与“个体差异”。5. 落地实战技巧离线视频回放、双指标曲线验证与跳帧优化5.1 改一行代码让系统回放视频实现自动化回归测试把主循环里的cap cv2.VideoCapture(0)换成cap cv2.VideoCapture(test.mp4)整个疲劳检测系统就变成离线批处理工具。配合前文的人工标注可以批量跑几十段视频把每段的检测结果写入 CSV之后调参不再需要人盯屏幕只需比较 CSV 里的tp/fp数字。源码包里如果自带了测试视频这是理解作者意图最快的入口。5.2 用 EAR 与 PERCLOS 双曲线图定位参数问题跑完离线视频后把每帧的 EAR 和滑窗 PERCLOS 分别存入列表用 matplotlib 画在同一张图。观察曲线形态能快速定位问题EAR 快速下探时 PERCLOS 缓慢爬升属于正常联动若 PERCLOS 频繁上扬但 EAR 没有明显下探多半是窗口里混入了人脸丢失期间的数据需要检查win.clear()分支是否生效若 EAR 曲线整体偏低但 PERCLOS 始终不触发则大概率是PERCLOS_ALARM设置得过高。5.3 低性能设备上的跳帧检测在大幅降低 CPU 占用的同时保持判定稳定树莓派或边缘盒子跑 FaceMesh 时帧率往往掉到 15 fps 以下这时最有效的优化是跳帧检测每 N 帧做一次完整的关键点提取中间帧沿用上一次的 EAR 值。疲劳判定是秒级决策丢失两帧的几何数据完全不影响结果但 CPU 占用率能降近一半。frame_idx 0 SKIP_FRAMES 1 # 每 2 帧检测一次0 表示不跳帧 last_ear 0.22 # 初始值取一个合理的睁眼值避免启动期误判 while cap.isOpened(): ret, frame cap.read() if not ret: break if frame_idx % (SKIP_FRAMES 1) 0: rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results face_mesh.process(rgb) if results.multi_face_landmarks: lm results.multi_face_landmarks[0].landmark last_ear (ear(lm, LEFT_EYE) ear(lm, RIGHT_EYE)) / 2.0 # 未检测帧沿用上轮的 EAR 值保证推入滑窗的数据连续 cur_ear last_ear frame_idx 1跳帧参数SKIP_FRAMES 1表示两帧里检测一帧中间一帧沿用旧值。这个值改成 2 时帧率提升更明显但耳根处小幅度位移造成的误差会被放大写入真正的源码前需要拿现场视频跑一遍确认告警不抖动。若设备性能允许首选方案其实是缩小输入分辨率到 640×480 再做检测分辨率降低对 EAR 比值的影响远小于跳帧带来的数据延迟双管齐下时先缩分辨率、再加跳帧。本文还有配套的精品资源点击获取
返回列表