
简介这份资源面向计算机视觉初学者与Python开发者提供在Ubuntu环境下基于OpenCV实现实时人眼识别、眨眼检测与闭眼检测的完整代码与教程。包内共61个文件以37个py脚本为核心配合12张png与4张jpg演示图、1个dat人脸关键点模型、1份pdf教程及1个md说明另有cfg、txt等配置文件压缩包约75.34MB结构清晰便于按模块查阅。技术路线采用Haar级联分类器定位眼睛结合帧差、光流或霍夫变换分析连续帧变化判定眨眼并通过瞳孔与眼睑可见性识别持续闭眼状态。教程覆盖依赖安装、代码运行与原理讲解读者可据此搭建实时检测流程理解关键点模型与分类器的配合方式并在此基础上扩展疲劳监测、人机交互等应用。目前已有4345人学习下载适合希望以实践案例入门计算机视觉的开发者参考。1. 从一帧摄像头画面说起人眼识别、眨眼与闭眼检测到底在做什么你打开笔记本摄像头画面里一张脸程序要在 30 毫秒内回答三个问题眼睛在哪、这一帧是睁还是闭、从睁到闭再睁算不算一次眨眼。这就是 Python OpenCV 实时人眼识别、眨眼检测、闭眼检测要解决的事。它不依赖深度学习大模型靠的是人脸检测器加眼部区域裁剪再用眼睛纵横比EAREye Aspect Ratio做几何判断整条链路在普通 CPU 上就能跑到 20 到 30 帧。适合谁做疲劳驾驶预警、在线考试防作弊、护眼提醒、人机交互 demo 的开发者以及刚学完 OpenCV 图像处理项目、想找一个能跑通又有成就感的实战练手的人。下面我按自己实际搭过的顺序把选型、代码、参数和翻车点讲清楚。2. 技术选型与原理为什么用 EAR 而不是直接上 CNN2.1 三种主流路线的取舍做人眼状态判断常见有三条路。第一条是 Haar 级联加模板匹配OpenCV 自带haarcascade_eye.xml零额外依赖但侧脸、戴眼镜、光照变化下误检率高闭眼时经常直接检测不到眼睛导致逻辑断掉。第二条是 EAR 几何法先用人脸关键点定位 68 个点取每只眼睛的 6 个点算纵横比睁眼时比值大、闭眼时比值骤降稳定且可解释。第三条是 CNN 分类精度最高但要 GPU、要标注数据、要训练对「实时 轻量」这个诉求属于杀鸡用牛刀。我一般选第二条。原因是 EAR 的计算量几乎可以忽略逻辑透明阈值调起来有据可依出问题能定位到具体是哪几个关键点飘了。人脸关键点检测用 dlib 的shape_predictor_68_face_landmarks.dat这是业界用了很多年的模型68 点里第 36 到 41 是左眼、42 到 47 是右眼正好够算 EAR。2.2 EAR 的数学定义单只眼睛取 6 个点编号 p1 到 p6顺序是左右眼角各一个上下眼睑各两个。EAR 公式是垂直距离之和除以两倍水平距离EAR (|p2-p6| |p3-p5|) / (2 * |p1-p4|)睁眼时上下眼睑拉开分子大EAR 通常在 0.25 到 0.35闭眼时上下贴合分子趋近于 0EAR 掉到 0.15 以下。两只眼睛各算一次取平均能抵消单侧关键点抖动。这个比值的好处是对人脸远近、轻微旋转不敏感因为分子分母同时缩放。2.3 眨眼判定的状态机光看单帧 EAR 不够摄像头噪声会让某一帧突然掉下去误判成眨眼。正确做法是维护一个计数器连续 N 帧 EAR 低于阈值认为进入闭眼状态之后连续 M 帧回到阈值以上认为眼睛重新睁开这一闭一开才算一次完整眨眼。N 和 M 一般取 2 到 3对应 30fps 下约 60 到 100 毫秒符合真实眨眼时长。闭眼检测则是另一个维度如果 EAR 持续低于阈值超过比如 1.5 秒那就不是眨眼是闭眼或困倦触发告警。2.4 环境准备与依赖安装先把环境搭好这一步翻车最多。Python 建议 3.8 及以上OpenCV 用opencv-python关键点用dlib。dlib 在 Windows 上直接 pip 装经常编译失败血泪经验是优先用 conda 或者预编译 wheel。# 建议先建虚拟环境避免污染全局 python -m venv eye_env # Windows 激活 eye_env\Scripts\activate # Linux / macOS 激活 source eye_env/bin/activate # 安装核心依赖 pip install opencv-python pip install dlib pip install numpy pip install scipy装完立刻验证别等到写代码才发现 cv2 没装上import cv2 import dlib import numpy as np print(opencv:, cv2.__version__) print(dlib:, dlib.__version__)如果报ModuleNotFoundError: No module named cv2说明装到了别的解释器里检查which python或where python是否指向虚拟环境。dlib 编译报错的话Linux 上先sudo apt install cmake build-essential再重装。这一步别省环境不通后面全是玄学问题。3. 从零跑通摄像头读取、关键点定位与 EAR 计算3.1 最小可运行骨架先写一个能打开摄像头、画出人脸框和眼睛关键点的版本确认链路通了再叠加逻辑。这个顺序很重要一上来就写完整状态机出错时你分不清是检测问题还是逻辑问题。import cv2 import dlib import numpy as np from scipy.spatial import distance as dist # 人脸检测器与68点关键点模型 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) # 左右眼在68点中的索引 LEFT_EYE list(range(36, 42)) RIGHT_EYE list(range(42, 48)) def eye_aspect_ratio(eye_points): # 垂直距离上眼睑两点与下眼睑两点 v1 dist.euclidean(eye_points[1], eye_points[5]) v2 dist.euclidean(eye_points[2], eye_points[4]) # 水平距离左右眼角 h dist.euclidean(eye_points[0], eye_points[3]) return (v1 v2) / (2.0 * h) cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) for face in faces: shape predictor(gray, face) pts np.array([(shape.part(i).x, shape.part(i).y) for i in range(68)]) left pts[LEFT_EYE] right pts[RIGHT_EYE] ear (eye_aspect_ratio(left) eye_aspect_ratio(right)) / 2.0 cv2.putText(frame, EAR: {:.2f}.format(ear), (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imshow(Eye, frame) if cv2.waitKey(1) 0xFF 27: break cap.release() cv2.destroyAllWindows()逻辑说明detector(gray, 0)里的 0 是上采样次数0 表示不放大速度快如果脸小检测不到可以改成 1代价是耗时翻倍。predictor返回的是 68 个点对象转成 numpy 数组方便切片。EAR 计算里dist.euclidean就是两点欧氏距离分子两个垂直距离相加分母是水平距离的两倍。跑起来后你应该能看到 EAR 在 0.3 附近波动闭眼时掉到 0.1 左右。3.2 关键参数怎么设参数含义推荐值调整方向上采样次数detector 第二参数0脸小改 1速度换精度EAR 阈值睁闭分界0.22眼镜党可降到 0.20闭眼帧数判定进入闭眼3抖动大就加到 4睁眼帧数判定重新睁开3与闭眼帧数对称闭眼告警秒数持续闭眼触发1.5疲劳场景可到 2.0阈值不是拍脑袋定的。正确做法是先跑一段只有睁眼的视频记录 EAR 的最小值再跑一段闭眼视频记录最大值两者之间取中间偏下一点。戴眼镜的人因为镜片反光EAR 整体会偏低阈值要相应下调这是最常见的翻车来源。3.3 把眨眼状态机接进去有了稳定的 EAR加状态机。核心是两个计数器加一个总眨眼数逻辑要写成「先判闭、再判开」顺序反了会漏计。EYE_AR_THRESH 0.22 # 睁闭阈值 EYE_AR_CONSEC_FRAMES 3 # 连续帧数 COUNTER 0 # 闭眼连续帧计数 TOTAL 0 # 累计眨眼次数 while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) for face in faces: shape predictor(gray, face) pts np.array([(shape.part(i).x, shape.part(i).y) for i in range(68)]) ear (eye_aspect_ratio(pts[LEFT_EYE]) eye_aspect_ratio(pts[RIGHT_EYE])) / 2.0 if ear EYE_AR_THRESH: COUNTER 1 # 持续闭眼累加 else: if COUNTER EYE_AR_CONSEC_FRAMES: TOTAL 1 # 从闭到开记一次眨眼 COUNTER 0 # 无论是否计数都清零 cv2.putText(frame, Blinks: {}.format(TOTAL), (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.putText(frame, EAR: {:.2f}.format(ear), (30, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imshow(Eye, frame) if cv2.waitKey(1) 0xFF 27: break逻辑说明COUNTER只在 EAR 低于阈值时累加一旦回到阈值以上先判断刚才闭眼是否够久够久才算真眨眼再清零。这样单帧噪声不会计数。参数上EYE_AR_CONSEC_FRAMES取 3 是经验值帧率越高可以适当加大。注意COUNTER 0必须放在 if 外面否则连续睁眼时计数器不清零下次闭眼会误判。4. 闭眼检测与告警把「眨眼」和「闭眼」分开4.1 时间维度的判定眨眼和闭眼的区别不在单帧 EAR而在持续时间。一次正常眨眼 100 到 400 毫秒闭眼犯困会超过 1 秒。所以闭眼检测要引入时间戳而不是只数帧。用time.time()记录进入闭眼状态的时刻每帧检查当前时间减去起始时间是否超过阈值。import time CLOSED_SECONDS 1.5 # 持续闭眼告警阈值 closed_start None # 闭眼起始时间戳 while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) for face in faces: shape predictor(gray, face) pts np.array([(shape.part(i).x, shape.part(i).y) for i in range(68)]) ear (eye_aspect_ratio(pts[LEFT_EYE]) eye_aspect_ratio(pts[RIGHT_EYE])) / 2.0 if ear EYE_AR_THRESH: if closed_start is None: closed_start time.time() # 刚进入闭眼记时间 elif time.time() - closed_start CLOSED_SECONDS: cv2.putText(frame, DROWSY!, (30, 90), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 3) else: closed_start None # 睁眼重置计时 cv2.imshow(Eye, frame) if cv2.waitKey(1) 0xFF 27: break逻辑说明closed_start为 None 表示当前是睁眼状态一旦 EAR 掉下去就记录起始时刻之后每帧比较时长。超过阈值就叠加告警文字。睁眼时把它重置为 None保证下次闭眼重新计时。这个写法比数帧更稳因为帧率会波动数帧在低帧率设备上会误报。4.2 用帧率换算而不是硬编码帧数如果你的设备帧率不是 30硬编码帧数就会偏。稳妥做法是动态测帧率把秒数换算成帧数。下面这段在循环里估算 FPS再据此调整计数阈值。prev_time time.time() fps 0.0 while True: ret, frame cap.read() if not ret: break now time.time() dt now - prev_time prev_time now if dt 0: fps 0.9 * fps 0.1 * (1.0 / dt) # 滑动平均抗抖动 # 用帧率把秒换算成帧数 consec_frames max(2, int(fps * 0.1)) # 约100毫秒 cv2.putText(frame, FPS: {:.1f}.format(fps), (30, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)逻辑说明fps用指数滑动平均系数 0.9 和 0.1 是经验值越大越平滑但响应越慢。consec_frames把 100 毫秒换算成帧数低帧率设备自动降低要求高帧率设备自动提高比写死 3 更通用。这个技巧在跨设备部署时特别有用。4.3 双眼不一致时的处理有时候一只眼被头发挡住关键点飘了两只眼 EAR 差很多。直接取平均会把好的那只也带偏。我的做法是加一致性检查两眼 EAR 差值超过 0.1 就只信较高的那只因为遮挡通常让 EAR 偏低。ear_left eye_aspect_ratio(pts[LEFT_EYE]) ear_right eye_aspect_ratio(pts[RIGHT_EYE]) if abs(ear_left - ear_right) 0.1: ear max(ear_left, ear_right) # 差异大信高的 else: ear (ear_left ear_right) / 2.0逻辑说明0.1 这个差值是实测出来的正常双眼 EAR 差异在 0.05 以内超过说明有一侧异常。取 max 相当于丢弃被遮挡的那只眼。这个处理在侧脸和刘海场景下能明显降低误报。5. 避坑与排查那些让我重跑一整天的细节5.1 关键点模型文件路径找不到现象运行报RuntimeError: Unable to open shape_predictor_68_face_landmarks.dat。原因是shape_predictor接收的是文件路径不是模型名程序在运行目录下找不到文件。解决把 dat 文件放在脚本同目录或者写绝对路径。注意这个文件解压后约 95MB别用错版本。5.2 摄像头打不开或读到黑屏现象cap.read()一直返回 False或者画面全黑。原因通常是摄像头被其他程序占用或者索引不对。解决先换索引试cv2.VideoCapture(1)再确认没有其他软件占着摄像头。Linux 上还要检查当前用户有没有 video 组权限没有就sudo usermod -aG video $USER后重新登录。5.3 戴眼镜时 EAR 整体偏低导致误判闭眼现象戴眼镜的人睁着眼EAR 却只有 0.18一直触发闭眼告警。原因是镜片反光和镜框遮挡让关键点内缩。解决把阈值从 0.22 下调到 0.18 到 0.20并且用 4.3 的一致性检查丢弃异常眼。更好的做法是让用户先做一次睁眼校准记录基线 EAR阈值取基线的 70%。5.4 眨眼计数偏多或偏少现象明明眨了一次计数加了 2 或 3。原因是阈值卡在 EAR 波动区间眼睛在阈值附近来回跳。解决加迟滞进入闭眼用低阈值退出闭眼用高阈值两个阈值差 0.03 左右避免临界抖动。另外确认COUNTER清零位置正确。5.5 帧率低导致告警延迟现象闭眼 2 秒了才报警。原因是主循环里做了太多耗时操作比如每帧都画大量关键点、每帧都做上采样。解决把 detector 上采样设为 0关键点绘制改成只画眼睛区域必要时降分辨率到 640x480。实测这样能把帧率从 12 提到 28。6. 进阶技巧让这套方案在真实场景里站得住跑通 demo 只是起点真正上线要解决鲁棒性。第一个技巧是加 EAR 基线自适应程序启动后前 3 秒让用户正常睁眼采集 EAR 均值作为基线阈值取基线的 0.7 倍。这样不同人、不同眼镜、不同光照都能自动适配比固定 0.22 靠谱得多。第二个技巧是头部姿态过滤用 solvePnP 估算头部朝向当人脸偏转超过 30 度时暂停判定因为侧脸关键点本身就不可信硬判只会误报。第三个技巧是把眨眼频率做成统计量正常人每分钟 15 到 20 次如果连续 30 秒低于 8 次即使没有长闭眼也提示疲劳这比单次闭眼告警更早发现困倦趋势。验证方法上我习惯录一段自己正常眨眼、故意闭眼、戴眼镜、侧脸的视频离线跑一遍统计误报和漏报把参数调到位再上实时。别只对着摄像头看几秒就下结论真实场景的坑都在长尾里。这套方案值不值得做如果你要的是轻量、可解释、能快速落地的眼睛状态检测它完全够用如果要的是复杂光照下的高精度那就得考虑关键点模型升级或加分类器。我自己踩过的最大教训是别急着调阈值先把关键点画出来看它稳不稳关键点飘了后面所有参数都是白调。希望帮到你。本文还有配套的精品资源点击获取