Python+OpenCV实时人眼识别与眨眼检测:基于EAR的疲劳监测源码实战
简介这份资源面向计算机视觉初学者与需要落地人脸眼部状态检测的开发者提供基于Python与OpenCV的实时人眼识别、眨眼检测和闭眼检测完整实现。代码在Ubuntu环境下开发运行借助Haar级联分类器定位眼睛区域并通过连续帧差异分析判断眨眼与持续闭眼状态可作为课程设计、疲劳监测原型或视觉入门练手项目。压缩包共61个文件约75.34MB以37个py源码文件为核心配合png、jpg示例图、dat预训练模型、pdf教程及md说明等覆盖从环境配置到代码原理的讲解目录结构清晰便于按模块查阅。目前已有4345人学习下载适合希望快速跑通眼部状态检测流程、理解帧差与特征分析思路的读者参考复用。1. 从摄像头到 EAR这套 PythonOpenCV 源码到底能跑出什么很多人第一次做疲劳驾驶或在线课堂注意力检测都会卡在同一个地方人脸框出来了眼睛在哪、什么时候眨、闭眼持续了多久全靠猜。这套「PythonOpenCV 实时人眼识别眨眼检测闭眼检测」源码解决的正是这个从「看到脸」到「判断眼睛状态」的最后一公里。它用 OpenCV 抓摄像头帧用 dlib 的 68 点人脸关键点定位眼部区域再通过 EAREye Aspect Ratio眼睛纵横比把「睁眼/闭眼」变成一个可阈值判断的数值。适合做课程设计、疲劳监测原型、网课专注度统计的从业者也适合刚学完 python 基础、想找一个能跑起来的 opencv 图像处理项目练手的人。整套逻辑不依赖深度学习训练CPU 上就能实时跑这是它比很多「大模型方案」更接地气的地方。2. 环境搭建与依赖选型为什么是 dlib 而不是纯 Haar2.1 三个核心库的分工与版本边界这套源码的依赖链其实很短opencv-python 负责视频采集和图像预处理dlib 负责关键点回归numpy 负责 EAR 的向量计算。很多人一上来就pip install opencv-python dlib结果在 Windows 上编译 dlib 直接翻车因为 dlib 的源码编译需要 CMake 和 C 编译环境。常见做法是先用 conda 装 dlib或者直接下预编译的 whl 包。# 推荐用 conda 建独立环境避免污染系统 python conda create -n eye_detect python3.8 conda activate eye_detect # opencv 用 pip 装即可dlib 走 conda 预编译 pip install opencv-python numpy conda install -c conda-forge dlib这里 python 3.8 是个稳妥选择dlib 和 opencv 的兼容轮子最全。如果你坚持用 pip 装 dlibWindows 下大概率会遇到CMake must be installed或Microsoft Visual C 14.0 is required这不是代码问题是编译链问题。参数上opencv-python 选 4.x 任意小版本都行dlib 选 19.22 以上关键点模型才稳定。2.2 关键点模型文件的获取与放置dlib 的 68 点检测靠一个预训练模型shape_predictor_68_face_landmarks.dat这个文件不在 pip 包里必须单独下载。源码包里一般会附带如果没有去 dlib 官方模型页拿。放的位置很关键很多人RuntimeError: Unable to open shape_predictor_68_face_landmarks.dat就是路径写错。import dlib import os # 用相对路径时确保工作目录和模型文件同级 MODEL_PATH os.path.join(os.path.dirname(__file__), shape_predictor_68_face_landmarks.dat) if not os.path.exists(MODEL_PATH): raise FileNotFoundError(f模型文件缺失: {MODEL_PATH}) detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(MODEL_PATH)逻辑说明get_frontal_face_detector()返回的是基于 HOG 的人脸检测器比 Haar 级联稳但比 CNN 慢一点CPU 上够用。shape_predictor加载模型后输入人脸框输出 68 个坐标点。参数上模型文件约 99MB别用 git 直接传容易超限。2.3 摄像头采集与帧率控制实时检测最怕卡顿采集环节要设对分辨率和缓冲。import cv2 cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30) if not cap.isOpened(): raise RuntimeError(摄像头打开失败检查索引或驱动) while True: ret, frame cap.read() if not ret: break frame cv2.flip(frame, 1) # 镜像符合直觉 # 后续检测逻辑640x480 是实时性和精度的平衡点再高 CPU 占用明显上升。cv2.flip镜像后左右眼索引不会乱因为 dlib 的点序是固定的。注意cap.read()返回 False 时要 break否则会死循环。3. EAR 算法落地从 68 点到眨眼计数的完整链路3.1 眼睛关键点索引与 EAR 公式dlib 68 点里左眼是 36-41右眼是 42-47各 6 个点。EAR 的定义是垂直距离和水平距离的比值import numpy as np from scipy.spatial import distance as dist def eye_aspect_ratio(eye): # eye 是 6 个 (x, y) 点顺序为顺时针 # 垂直距离上下两组点 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逻辑说明睁眼时 EAR 约 0.25-0.35闭眼时降到 0.1 以下。A、B 是上下眼睑的距离C 是眼角距离除以 2C 做归一化抵消人脸远近影响。参数上dist.euclidean来自 scipy如果不想装 scipy用 numpy 手写np.linalg.norm也行。3.2 眨眼判定阈值、连续帧与计数器单帧 EAR 低于阈值不算眨眼可能是噪声。标准做法是连续 N 帧低于阈值才计一次眨眼。EYE_AR_THRESH 0.25 # EAR 阈值 EYE_AR_CONSEC_FRAMES 3 # 连续帧数 COUNTER 0 TOTAL 0 def update_blink(ear): global COUNTER, TOTAL if ear EYE_AR_THRESH: COUNTER 1 else: if COUNTER EYE_AR_CONSEC_FRAMES: TOTAL 1 COUNTER 0 return TOTAL逻辑说明COUNTER记录连续低 EAR 帧数一旦 EAR 回升且之前累计超过 3 帧判定为一次完整眨眼。阈值 0.25 是经验值戴眼镜或光线暗时要下调到 0.2。连续帧数 3 对应 30fps 下约 100ms符合真实眨眼时长。3.3 闭眼检测与疲劳预警的衔接闭眼检测和眨眼共用 EAR区别在持续时间。眨眼是瞬时闭眼是持续低 EAR。CLOSED_EYE_FRAMES 15 # 约 0.5 秒 closed_counter 0 def check_drowsy(ear): global closed_counter if ear EYE_AR_THRESH: closed_counter 1 if closed_counter CLOSED_EYE_FRAMES: return True # 触发疲劳预警 else: closed_counter 0 return False逻辑说明15 帧在 30fps 下是 0.5 秒超过这个时长基本可判定为闭眼而非眨眼。参数可按场景调驾驶场景建议 0.4-0.6 秒网课场景可放宽到 1 秒。返回 True 后可以接蜂鸣器或日志。3.4 把检测结果画回画面可视化是调试的关键不然你不知道点抓没抓准。from scipy.spatial import ConvexHull def draw_eye(frame, eye_points): hull ConvexHull(eye_points) hull_points eye_points[hull.vertices] cv2.drawContours(frame, [hull_points], -1, (0, 255, 0), 1) return frame # 主循环里 left_eye np.array([(shape.part(i).x, shape.part(i).y) for i in range(36, 42)]) right_eye np.array([(shape.part(i).x, shape.part(i).y) for i in range(42, 48)]) ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 frame draw_eye(frame, left_eye) frame draw_eye(frame, right_eye) cv2.putText(frame, fEAR: {ear:.2f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)逻辑说明ConvexHull把 6 个点包成凸多边形画出来就是眼眶轮廓。左右眼 EAR 取平均比单眼稳。putText把 EAR 实时显示方便你调阈值。4. 避坑与排查那些让检测失效的细节4.1 现象EAR 一直高于阈值闭眼也不降原因关键点没对准眼睛常见于侧脸、刘海遮挡或模型文件版本不对。解决先单独跑一帧把 68 点全画出来确认 36-47 落在眼睛上。如果点飘到眉毛说明人脸框检测偏了可以调detector的upsample参数或换 CNN 检测器。4.2 现象眨眼计数疯狂累加原因阈值太高或连续帧数太小噪声被当成眨眼。解决把EYE_AR_THRESH从 0.25 降到 0.2EYE_AR_CONSEC_FRAMES从 3 提到 4-5。同时检查光照逆光会让 EAR 整体偏低。4.3 现象摄像头帧率掉到个位数原因每帧都跑 dlib 检测CPU 吃满。解决不要每帧都检测人脸可以隔帧检测中间帧用上一帧的关键点做光流跟踪。或者把分辨率降到 320x240检测完再放大显示。4.4 现象ModuleNotFoundError: No module named cv2原因装的是 opencv-python 但环境不对或者装了 opencv-contrib 冲突。解决pip uninstall opencv-python opencv-contrib-python后只装opencv-python。conda 环境里确认which python指向当前环境。4.5 现象模型文件加载报错但文件明明存在原因路径含中文或空格dlib 的 C 层读不了。解决把模型文件和脚本放在纯英文路径下用os.path.abspath打印真实路径确认。5. 进阶技巧让这套源码从 demo 变成能用的工具5.1 用 EAR 基线自适应替代固定阈值固定阈值在不同人、不同光线下表现差异很大。更稳的做法是前 3 秒采集睁眼 EAR 均值作为基线阈值取基线的 70%。baseline_samples [] baseline_ready False def adaptive_threshold(ear): global baseline_ready if not baseline_ready: baseline_samples.append(ear) if len(baseline_samples) 90: # 约 3 秒 baseline np.mean(baseline_samples) baseline_ready True return baseline * 0.7 return 0.25 # 默认值 return baseline * 0.7逻辑说明90 帧在 30fps 下是 3 秒足够采集自然睁眼状态。基线乘 0.7 是经验系数闭眼时 EAR 通常降到基线的 40%-60%。这个改动能让同一套代码适应不同人不用手动调参。5.2 双眼一致性校验排除误检单眼被遮挡时另一只眼的 EAR 会误导判断。加一个双眼差值校验def is_valid_eye_state(left_ear, right_ear): # 双眼 EAR 差异过大说明一只眼被遮挡或检测异常 if abs(left_ear - right_ear) 0.1: return False return True逻辑说明正常睁眼或闭眼时双眼 EAR 接近。差值超过 0.1 时大概率是单眼关键点飘了这一帧不参与眨眼计数避免误判。5.3 用队列平滑 EAR 曲线原始 EAR 有抖动用固定长度队列做滑动平均曲线更干净。from collections import deque ear_history deque(maxlen5) def smooth_ear(ear): ear_history.append(ear) return np.mean(ear_history)逻辑说明deque的maxlen5自动丢弃旧值滑动窗口平均能压掉单帧噪声代价是引入约 5 帧延迟实时性要求极高的场景可以降到 3。5.4 验证方法用录制视频回放代替实时调试实时调参很痛苦因为眨眼瞬间就过去了。把一段包含多次眨眼的视频存下来用cv2.VideoCapture(test.mp4)回放逐帧打印 EAR你就能清楚看到阈值该设在哪。cap cv2.VideoCapture(blink_test.mp4) frame_idx 0 while True: ret, frame cap.read() if not ret: break # 检测逻辑同上 print(fframe {frame_idx}, EAR{ear:.3f}) frame_idx 1逻辑说明回放视频能复现方便对比不同阈值的效果。建议录一段正常眨眼、一段闭眼 2 秒、一段侧脸覆盖主要边界。5.5 一个我踩过的坑最早我把EYE_AR_CONSEC_FRAMES设成 1结果每帧 EAR 波动都计一次眨眼一分钟跑出 200 多次。后来改成 3 并加了双眼校验才稳定在正常范围。从那以后我每次调这类阈值都强制先用录制视频跑一遍全流程确认计数曲线合理再上实时。希望这套源码和上面的参数思路能帮你少走几段弯路。本文还有配套的精品资源点击获取