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

资讯详情

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

Python+dlib实现疲劳检测:基于EAR/MAR与PERCLOS的图像处理实战

Python+dlib实现疲劳检测:基于EAR/MAR与PERCLOS的图像处理实战 简介面向数字图像处理课程设计与毕业设计的Python疲劳检测项目包含完整可运行源码、结果演示视频与配套文档适合计算机相关专业学生完成课程作业、项目答辩或算法学习。代码基于Python实现覆盖眨眼、打哈欠、打电话、侧视等多类疲劳状态检测场景配有240段AVI格式演示录屏可直观对照算法输出与实际情况另有6个Python源码文件、PDF/DOCX说明文档、Markdown笔记及数据文件方便理解流程、复现结果与二次开发。资源共254个文件压缩包约300MB目录结构清晰各类型素材按功能归置。已有541人学习浏览代码经过多轮测试、运行稳定答辩评审平均分达96分可直接部署或在此基础上扩展功能是快速完成课程大作业与答辩展示的实用参考资料。1. 数字图像处理课程大作业用Python做疲劳检测先拆清楚要交什么课程大作业里疲劳检测是出现频率最高的题目之一但很多人把它做成了“套一个现成模型”的演示答辩时一问 EAR 是什么就答不上来。用 Python 做疲劳检测本质是一条完整的数字图像处理流水线人脸检测、灰度变换、特征点提取、几何特征计算、时序判定最后把报警状态画回视频帧。它能用普通摄像头实时识别眨眼和打哈欠代码量控制在三百行左右却覆盖了冈萨雷斯《数字图像处理》里最常考的考点。下面按选型、代码、参数、文档的顺序把一套可以直接跑、可以直接写进课程设计报告的方案完整讲一遍新手能照着做做过的也能对照参数边界和坑。2. 疲劳检测大作业的技术选型与EAR原理为什么是dlib加OpenCV2.1 三条技术路线的对比课程评分点决定选型疲劳检测在工业界有很成熟的实现但课程大作业考察的是“你懂不懂图像处理”不是“你能调到多准”。常见的路线可以分成三类第一类用 dlib 这样的人脸关键点检测器拿到 68 个关键点再根据眼睛和嘴部的几何比例计算 EAR、MAR 与 PERCLOS第二类用 CNN 对裁剪出的眼睛区域做睁眼/闭眼二分类再对每帧结果投票第三类接脑电、心电或手环传感器绕开图像处理直接读生理信号。三类方案里CNN 方案需要几千张标注眼图训练过程对算力和时间都有要求答辩时模型内部是黑盒传感器方案直接偏离课程主题。dlib 方案从摄像头取帧开始每一步都能对应到数字图像处理课程里的一个知识点评委好提问也好打分。技术路线疲劳特征来源数据要求实现成本答辩友好度dlib关键点 EAR/PERCLOS双眼与嘴部几何比例一段测试视频低模型现成高每步可解释CNN眼部二分类眼部图像纹理上万张标注图高需训练调参低特征难解释生理传感器心电/脑电/肌电硬件设备与接口中低偏离图像处理从数字图像处理课程主线来看dlib 人脸检测解决的是“目标定位”问题68 个关键点则是特征点的典型应用冈萨雷斯教材里关于特征提取与图像分割的内容正好能对上。选这条路至少在方法论上和课程大纲是齐的。2.2 EAR 与 MAR 的计算原理为什么用纵横比而不是像素距离dlib 的 68 点人脸关键点模型里下标 36 到 41 对应图像左侧眼睛的六个点42 到 47 对应右侧眼睛48 到 54 是嘴部外轮廓。眼睛六点按顺序分别是内眼角、上眼皮内侧、上眼皮外侧、外眼角、下眼皮外侧、下眼皮内侧。EAR 的定义是把两个竖直方向距离的均值除以水平方向距离EAR (|p2−p6| |p3−p5|) / (2·|p1−p4|)。用比值而不是绝对值是为了消除尺度影响。人脸离摄像头近时水平和竖直距离同时变大比值基本不变人脸轻微转动时六点之间的相对关系也能抵消一部分投影误差。这正是图像处理里“归一化特征”的思路也是大作业报告里能展开写的一段原理。人正常睁眼时 EAR 在 0.25 到 0.35 之间闭眼时会掉到 0.10 以下嘴部同理打哈欠时上下嘴唇距离被拉大嘴部比值从平时的 0.2 左右跳到 0.6 以上。下面把这两个特征量写成独立函数主程序会直接调用。import numpy as np # 传入单只眼睛的六个点坐标顺序为 36~41 或 42~47 def eye_aspect_ratio(eye_points: np.ndarray) - float: p1, p2, p3 eye_points[0], eye_points[1], eye_points[2] p4, p5, p6 eye_points[3], eye_points[4], eye_points[5] vertical np.linalg.norm(p2 - p6) np.linalg.norm(p3 - p5) horizontal 2.0 * np.linalg.norm(p1 - p4) return vertical / horizontal # 嘴部只用四个外轮廓点两侧嘴角与上下嘴唇中点 def mouth_aspect_ratio(landmarks: np.ndarray) - float: top landmarks[51] bottom landmarks[57] left landmarks[48] right landmarks[54] return np.linalg.norm(top - bottom) / np.linalg.norm(left - right)eye_aspect_ratio 的输入必须是同一只眼睛的六个点顺序不能乱因为 p2−p6、p3−p5 要求是上下对应的点对点对错位会把竖直距离算成斜线距离。mouth_aspect_ratio 直接使用 68 点坐标系里的固定编号51 和 57 分别是上嘴唇与下嘴唇的外轮廓中点48 和 54 是两侧嘴角。两个函数返回的都是无量纲比值测试视频的分辨率、摄像头远近都不会影响量纲阈值因此能跨设备复用。2.3 PERCLOS 判定标准疲劳是一段时间窗里的统计量单帧 EAR 低并不等于疲劳。正常眨眼也会让 EAR 穿过阈值眨眼通常持续 100 到 400 毫秒而疲劳导致的闭眼往往超过 1 秒。工程上普遍采用 PERCLOSPERcentage of eye CLOSure标准先定义“眼睛闭合”眼睑遮住瞳孔 80% 以上就算闭合帧再统计一个时间窗口内闭合帧占总帧数的比例P80 阈值一般取 0.4即窗口内闭眼帧占比超过 40% 判定为疲劳。大作业里实现 PERCLOS 有两种方式简化版是统计 EAR 连续低于阈值的帧数超过连续帧数阈值就报警标准版是维护一个 30 到 60 帧的滑动窗口窗口内闭合帧占比超过阈值才算疲劳。标准版对偶发闭眼更鲁棒报告的实验部分写起来也更有料推荐直接按第二种实现。实现时注意用 collections.deque 固定窗口长度每来一帧推入当前闭合状态并弹出最旧状态再统计窗口内 True 的比例这样时间窗是滑动的不会出现“程序跑得越久越容易报警”的错误。这个细节答辩时经常被问到。3. 基于Python的疲劳检测源代码主程序环境配置与EAR状态机实现3.1 Python 环境配置与依赖安装opencv/dlib/numpy 怎么配合这个项目只需要三个核心依赖opencv-python 负责读摄像头、灰度化、画框和显示dlib 提供正面人脸检测器和 68 点关键点预测器numpy 负责坐标数组运算。Python 安装好后在项目目录里准备一个虚拟环境然后执行一行命令装齐pip install opencv-python numpy dlibdlib 在 Windows 上依赖 CMake 和 C 编译环境装之前先确认 Visual Studio 的“使用 C 的桌面开发”组件已经勾选。装不上不要死磕两个替代方案一是用 conda 的 dlib 预编译包二是换 mediapipe 的 FaceMesh 取关键点。FaceMesh 返回的是 468 点眼睛和嘴部的点编号与 dlib 不同但 EAR/MAR 的算法逻辑完全一样只需要重新映射点数。下一步准备关键点模型文件 shape_predictor_68_face_landmarks.dat这是 dlib 官方模型库里的通用模型下载后放到项目根目录或 models 目录程序里用相对路径引用。提示模型文件体积不小建议固定在 models 目录并在 README 里写明获取方式避免换电脑后找不到模型文件导致程序直接退出。3.2 疲劳检测源代码主循环从视频帧到报警状态下面是完整的可运行版本。整个程序只有两个状态机一个统计眼睛连续低 EAR 的帧数一个统计嘴巴连续高 MAR 的帧数。两个状态机独立判定任何一方连续满足就切换报警状态避免单帧抖动造成的误判。视频来源可以是摄像头也可以改成录制好的测试视频路径。import cv2 import numpy as np import dlib # 坐标区间36~41 左眼42~47 右眼48~54 嘴部外轮廓 LEFT_EYE list(range(36, 42)) RIGHT_EYE list(range(42, 48)) EAR_THRESH 0.22 # EAR 低于该值认为眼睛接近闭合 EAR_CONSEC_FRAMES 3 # 连续 3 帧低 EAR 才算一次闭眼 MAR_THRESH 0.55 # 嘴部纵横比高于该值认为嘴张得很大 MAR_CONSEC_FRAMES 5 # 连续 5 帧高 MAR 才算一次打哈欠 def eye_aspect_ratio(eye_points: np.ndarray) - float: p1, p2, p3 eye_points[0], eye_points[1], eye_points[2] p4, p5, p6 eye_points[3], eye_points[4], eye_points[5] vertical np.linalg.norm(p2 - p6) np.linalg.norm(p3 - p5) horizontal 2.0 * np.linalg.norm(p1 - p4) return vertical / horizontal def mouth_aspect_ratio(landmarks: np.ndarray) - float: top, bottom landmarks[51], landmarks[57] left, right landmarks[48], landmarks[54] return np.linalg.norm(top - bottom) / np.linalg.norm(left - right) def main(video_path0, model_pathmodels/shape_predictor_68_face_landmarks.dat): cap cv2.VideoCapture(video_path) detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(model_path) ear_counter 0 # 连续低 EAR 帧计数 mar_counter 0 # 连续高 MAR 帧计数 state normal while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 1) for face in faces: shape predictor(gray, face) pts np.array([[p.x, p.y] for p in shape.parts()], dtypefloat) ear (eye_aspect_ratio(pts[LEFT_EYE]) eye_aspect_ratio(pts[RIGHT_EYE])) / 2.0 mar mouth_aspect_ratio(pts) if ear EAR_THRESH: ear_counter 1 if ear_counter EAR_CONSEC_FRAMES: state sleepy else: ear_counter 0 if state sleepy: state normal if mar MAR_THRESH: mar_counter 1 if mar_counter MAR_CONSEC_FRAMES: state yawn else: mar_counter 0 cv2.putText(frame, fEAR{ear:.2f} MAR{mar:.2f} {state}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(fatigue detection, frame) if cv2.waitKey(1) 0xFF 27: break cap.release() cv2.destroyAllWindows() if __name__ __main__: main(video_path0, model_pathmodels/shape_predictor_68_face_landmarks.dat)主循环里有三个点最值得注意。第一detector(gray, 1) 的第二个参数 1 表示对输入图像做一次上采样再检测能找回更小的人脸代价是每帧多花几毫秒摄像头场景推荐保持 1。第二EAR 取了左右眼的平均值原因是一只眼睛被头发或手势挡住时另一只眼睛仍有效平均值不会瞬间跌到异常区间如果需求是“单眼闭合就报警”把 mean 换成 min 即可。第三状态机里加了“由 sleepy 恢复到 normal”的分支报警是边沿触发而不是电平触发连续疲劳时画面不会反复闪现 normal。测试文件输入时把 video_path 换成视频文件路径ESC 键退出用的是 ASCII 27。3.3 多人脸与关键点可视化别让侧脸干扰判断课程演示环境里经常同时出现多张脸get_frontal_face_detector 会返回所有检测矩形。盲目遍历会拿检测到的第一张脸做判断更保险的做法是选面积最大的矩形因为进入画面的人脸通常离摄像头最近也是演示的主角。同时在帧上把 68 个关键点画出来能直观确认模型没有跟丢。# 多人脸时只保留面积最大的脸 if len(faces) 1: face max(faces, keylambda r: r.width() * r.height()) # 把关键点逐帧画到画面上 for pt in pts: x, y int(pt[0]), int(pt[1]) cv2.circle(frame, (x, y), 1, (0, 0, 255), -1)选择面积最大的人脸有个前提画面里不能出现“大头小脸”的反差如果后排有人用大屏幕显示自己的脸部特写前排真人反而会被忽略。这种情况最直接的解法是限定 ROI只把画面中间区域传给检测器。另外侧脸超过约 60 度时 68 点模型会丢失半边关键点EAR 会计算得异常偏低容易被误判成闭眼。对应措施是加一个下限保护EAR 低于 0.05 时认为是“关键点不可信”跳过这一帧不参与统计而不是把它当成闭眼帧累积。4. 疲劳检测参数标定与大作业文档说明阈值、CLAHE和报告结构4.1 疲劳检测参数标定EAR 阈值、连续帧数、MAR 阈值怎么设代码里的四个阈值直接决定误报率和漏报率课程报告里一定要有一张参数实验表。先把参数的作用与常规范围列出来参数推荐范围作用调错方向的表现EAR_THRESH0.20 ~ 0.25判定单帧眼睛是否闭合调太灵敏眨眼就报警调太迟钝闭眼不报警EAR_CONSEC_FRAMES2 ~ 5连续多少帧算闭眼调小响应快但误报多调大漏掉短闭眼MAR_THRESH0.50 ~ 0.70判定嘴巴是否大张不同人脸型差异大必须实测MAR_CONSEC_FRAMES4 ~ 8连续多少帧算哈欠调小说话也报警调大哈欠响应延迟高不建议直接抄网上代码里的 0.22因为每个人睁眼大小不一样戴眼镜、贴双眼皮、内双都会改变 EAR 的分布。这里给出一个自动标定方法用正常看屏幕的状态录 10 秒视频统计所有帧的 EAR 均值与标准差把阈值设为“均值减两倍标准差”。正常睁眼时的 EAR 波动近似正态分布均值减两个标准差能覆盖正常眨眼的轻微波动低于这个值的帧才视为异常闭眼。def auto_calibrate(video_path: str, model_path: str, sample_frames: int 300) - float: 统计清醒状态视频的 EAR 分布返回推荐阈值 mean - 2*std detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(model_path) cap cv2.VideoCapture(video_path) ears [] while len(ears) sample_frames: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 1) if not faces: continue face max(faces, keylambda r: r.width() * r.height()) shape predictor(gray, face) pts np.array([[p.x, p.y] for p in shape.parts()], dtypefloat) ears.append((eye_aspect_ratio(pts[LEFT_EYE]) eye_aspect_ratio(pts[RIGHT_EYE])) / 2.0) ears np.array(ears) return float(ears.mean() - 2.0 * ears.std())sample_frames 取 300 帧按 30fps 算正好是 10 秒太少统计不稳定太多录视频费时间。标定用的视频必须是清醒、正常睁眼状态不能夹带故意眯眼的镜头否则标出来的阈值偏低疲劳时反而报不出来。标定完成后拿这个结果回放一段模拟疲劳的视频看是否在闭眼开始后一秒内触发报警再手工按 0.01 的粒度微调就够用了。4.2 光照不均与丢点直方图均衡化和下限保护宿舍灯光、教室逆光、屏幕反光都会让 dlib 在光线差的区域检测不到人脸或者关键点剧烈抖动。转灰度后先做 CLAHE 自适应直方图均衡化是数字图像处理课程里“图像增强”章节的标准操作既能提高检测鲁棒性又能在报告里作为独立小节展开写。clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray clahe.apply(gray)clipLimit 是对比度限制阈值2.0 表示抑制裁切后的直方图峰值防止局部过曝区域被过度增强tileGridSize 是局部直方图均衡的网格大小8×8 对 640×480 的画面比较合适网格越小局部细节越突出但计算量越大。使用 CLAHE 之后逆光场景下脸部轮廓明显清晰关键点抖动也能肉眼可见地减少。另外提醒一点在 720p 以上的视频上跑检测建议先把帧缩放到 640 宽再送进 detector人脸检测本身不依赖原分辨率缩小一档能让 FPS 从十几帧提到三十帧左右。4.3 大作业源代码目录与文档说明的写法源代码的目录结构最好让答辩老师一眼看懂。课程评审一般先看目录再看报告结构清晰能省掉很多口头解释。推荐下面这种布局fatigue-detection/ ├── src/ │ ├── main.py # 主程序视频/摄像头入口状态机与报警 │ ├── features.py # EAR、MAR、PERCLOS 特征量计算 │ └── calibrate.py # 阈值自动标定脚本 ├── models/ │ └── shape_predictor_68_face_landmarks.dat # dlib 关键点模型 ├── data/ │ ├── normal.avi # 清醒状态测试视频 │ └── sleepy.avi # 模拟疲劳测试视频 ├── docs/ │ └── 课程设计报告.md └── requirements.txt文档说明也就是课程报告按五个部分组织第一问题定义与功能清单用一两段话说明疲劳检测解决什么问题、程序能输出什么第二算法流程从灰度化、CLAHE 增强、人脸检测、关键点定位到 EAR/PERCLOS 计算每一步对应教材里的概念第三参数实验把 4.1 的表和图放进来对比不同阈值下的误报与漏报第四结果展示贴正常状态、闭眼报警、哈欠报警三张截图外加 EAR 曲线第五不足与改进承认侧脸、遮挡、暗光下的缺陷再写一句更优的人脸关键点模型或 CNN 融合方向。把这五块写完源代码和文档说明就都齐了剩下的功夫在实验数据上。5. 疲劳检测实验结果的曲线可视化与量化验证技巧5.1 把 EAR 曲线和闭眼标记画成一张图答辩时最有力的语言不是“准确率 95%”而是一张 30 秒的 EAR 曲线。横轴是帧号纵轴是 EAR清醒段的曲线像一条带锯齿的直线锯齿是每次眨眼留下的 V 形低谷疲劳段的曲线则长时间压在阈值线以下。评委扫一眼就能看出算法确实在跟踪状态而不是从别处截来的图。import matplotlib matplotlib.use(Agg) # 无界面环境直接保存图片 import matplotlib.pyplot as plt def draw_ear_curve(ears: list, threshold: float, save_path: str ear_curve.png): plt.figure(figsize(10, 4)) plt.plot(ears, labelEAR, colortab:blue) plt.axhline(threshold, colortab:red, linestyle--, labelthreshold) plt.xlabel(frame) plt.ylabel(EAR) plt.legend() plt.grid(alpha0.4) plt.savefig(save_path, dpi150)把主循环里每一帧的 ear 值 append 到一个列表录制结束后调用这个函数即可。为了让实验可复现建议把 EAR 时间序列同时写进 CSV报告里就能引用这张图和原始数据。闭眼区间可以在图上用 fill_between 手工标注也可以在程序里直接记录 state 切换的帧号两种方式都行后者更省事。5.2 用自标定视频和 FPS 做一次性验证准备一段 30 到 60 秒的自录视频作为测试集人工标注每一段闭眼的起始帧和结束帧然后让程序按帧输出预测结果。统计两个数闭眼帧的召回率即人工标为闭眼且程序也报闭眼的帧数占比误报次数即清醒段被误报为睡眠状态的次数。召回率用来判断算法会不会漏报误报次数用来判断会不会吵到人。两个数字写进报告比“基本能检测”四个字有说服力得多。FPS 用 cv2.getTickCount 包住每一帧的检测部分来计算把 FPS 和阈值实验放在同一张表里顺带说清 CLAHE 到底带来多大耗时。如果测得的结果是 FPS 偏低优先降分辨率而不是换模型640×480 下 dlib 正面人脸检测在普通笔记本上能跑到 25 帧以上这个数字本身就够写进报告的结论部分了。本文还有配套的精品资源点击获取
返回列表