2026/11/6 12:32:59

Python+OpenCV实时人眼识别与眨眼闭眼检测实战

Python+OpenCV实时人眼识别与眨眼闭眼检测实战 简介这是一份面向Python初学者与计算机视觉入门开发者的实战源码包围绕OpenCV实现实时人眼定位、眨眼判断与闭眼状态识别可用于课堂演示、课程设计、视觉算法练手及摄像头监测原型验证。代码以Haar级联分类器完成眼部检测结合连续帧差异、光流或霍夫变换分析眨眼并配置六十八点面部关键点模型附带Ubuntu环境下的依赖说明。压缩包共61个文件约75.34MB主体为37个Python脚本另有12张png、4张jpg示例图以及模型文件、pdf教程和md说明按源码、教程与依赖库分区组织便于对照查阅和复现实验。已有4369人学习读者可从教程理解分类器调用、关键点对齐与闭眼判定思路并借助示例图和脚本搭建自己的实时检测工程完成从原理学习到项目改造的过渡也可作为课程作业与毕业设计前期验证的参考资料。1. 从摄像头到眨眼判定这套 PythonOpenCV 人眼识别项目适合谁Python 和 OpenCV 做实时人眼识别、眨眼检测、闭眼检测听起来像三个模型叠在一起其实主线可以很朴素摄像头取帧人脸检测框出脸眼睛检测框出眼眨眼检测看眼睛区域纵横比在连续帧里的起伏闭眼检测则看这个比值持续低于阈值多久。这个项目适合手里有课程设计、疲劳监测原型、互动 Demo 需求想用一台普通摄像头和一份能改的源码快速跑通的人。它不追求论文级精度优势是链路短、依赖少、每一帧都能可视化。读下去你会看到目录怎么拆、EAR 怎么算、阈值怎么调以及我踩过的那些摄像头和光照的坑。2. 项目骨架与技术选型Haar 级联、关键点与 EAR 指标怎么配合这份源码的主线不是训练模型而是把 OpenCV 的检测能力和一个可解释的几何指标拼成实时管线。人脸检测负责缩小搜索范围眼睛检测负责拿到局部区域眨眼判定负责把单帧数值变成连续动作。三者拆开写后面换检测器、调阈值、加报警都不会牵一发动全身。我一般先把项目按“采集层、检测层、判定层、展示层”四块看再决定改哪一块这样不容易把摄像头问题误判成算法问题。2.1 目录结构与模块职责常见做法是按职责拆文件而不是把所有逻辑塞进一个main.py。下面这种结构在课程设计和原型项目里很常见文件名可以不同但分层思路基本一致eye_blink_demo/ ├── main.py # 入口参数解析、摄像头循环、按键退出 ├── config.py # 阈值、分辨率、模型路径等集中配置 ├── detectors/ │ ├── face_detector.py # 人脸检测封装 │ └── eye_detector.py # 眼睛区域检测或关键点封装 ├── logic/ │ ├── ear.py # 眼睛纵横比计算 │ └── blink_state.py # 眨眼/闭眼状态机 ├── utils/ │ ├── drawing.py # 框、文字、FPS 绘制 │ └── camera.py # 摄像头或视频文件读取 ├── models/ │ ├── haarcascade_frontalface_default.xml │ └── haarcascade_eye.xml └── requirements.txt这个结构的价值在于“检测”和“判定”解耦。face_detector.py只关心输入一帧灰度图、输出人脸框列表eye_detector.py只关心输入人脸 ROI、输出眼睛框或关键点blink_state.py只接收每帧的 EAR 数值输出眨眼次数、闭眼时长和当前状态。后面想把 Haar 换成人脸关键点检测器只需要改检测层状态机不用动。config.py集中放阈值也很关键实时项目调参频繁阈值散落在循环里改一次就要全局搜索血泪经验是先把参数收口。提示如果源码是单文件版本也建议先按这四层在脑子里画边界二次开发时再拆文件不要一上来大改。2.2 技术选型为什么不是所有方案都用深度学习这个项目标题里明确是 PythonOpenCV所以最稳的基线是 OpenCV 自带 Haar 级联人脸一个模型眼睛一个模型依赖少CPU 能跑适合实时预览。它的短板也明显侧脸、遮挡、强逆光下容易漏检眼睛框会跳。常见增强做法是引入 68 点人脸关键点模型用眼睛周围的 6 个点算 EAR数值比矩形框更稳再重一点的做法是轻量关键点网络精度更好但部署和依赖会增加。方案依赖实时性稳定性适合场景OpenCV Haar 人脸眼睛低好正脸尚可侧脸弱课程演示、快速原型人脸关键点 EAR中较好戴眼镜、小角度更稳疲劳监测原型轻量深度学习检测较高看硬件复杂光照更好产品化验证选型时不要只看精度先看你的摄像头位置和用户会不会正脸。固定正脸、光照稳定的场景Haar 足够跑通用户会转头、戴眼镜、离摄像头忽远忽近就要尽早考虑关键点方案。这个项目的教学价值在于 EAR 判定逻辑检测器只是前端换检测器时保持get_eye_region(frame)这类接口不变后面就能平滑替换。2.3 从采集到判定的数据流实时人眼识别的主循环可以压缩成下面这段骨架。它不包含具体检测实现但把每一帧的流转顺序定死了取帧、转灰度、检测人脸、取眼睛 ROI、算 EAR、更新状态、绘制结果。import cv2 cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ok, frame cap.read() if not ok: break # 读帧失败就退出避免后面空帧报错 frame cv2.flip(frame, 1) # 镜像预览更符合直觉 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) # 轻度均衡缓解背光 faces detect_faces(gray) # 检测层返回人脸框列表 for face in faces: eye_roi crop_eye_region(gray, face) ear compute_ear(eye_roi) # 判定层输入单帧数值 state blink_state.update(ear) # 状态机输出眨眼/闭眼 draw_result(frame, face, state) cv2.imshow(eye blink demo, frame) if cv2.waitKey(1) 0xFF 27: # ESC 退出 break cap.release() cv2.destroyAllWindows()参数上CAP_PROP_FRAME_WIDTH和HEIGHT设成 640×480 是实时检测的常用起点分辨率再高Haar 检测耗时上升很快。equalizeHist不是必须但在室内背光下能减少“脸亮眼睛黑”的翻车。cv2.flip(frame, 1)只影响预览不影响检测数值注意如果你后续要保存视频保存镜像后的帧更符合用户看到的画面。waitKey(1)控制刷新按 ESC 退出如果你在循环里做重检测这个值不要设太大否则窗口会卡。3. 核心模块拆解人脸检测、眼睛 ROI 与眨眼状态机这一章把核心代码拆开。人脸检测负责“找到脸”眼睛 ROI 负责“缩小战场”EAR 负责“把眼睛开合变成数字”状态机负责“把数字变成动作”。四段里最容易出问题的是状态机因为单帧 EAR 会抖直接拿阈值比较会出现一次眨眼计数好几次的情况。我的做法是阈值、连续帧数、冷却时间三个参数一起看不要只调一个。3.1 人脸与眼睛检测Haar 级联的初始化与参数Haar 级联的初始化放在循环外不要每帧加载 XML。下面这段是常见写法先检测人脸再在人脸框上半部分检测眼睛减少误检。import cv2 face_cascade cv2.CascadeClassifier( models/haarcascade_frontalface_default.xml ) eye_cascade cv2.CascadeClassifier( models/haarcascade_eye.xml ) def detect_faces(gray): faces face_cascade.detectMultiScale( gray, scaleFactor1.1, # 每次缩放比例越小越慢但更容易检出 minNeighbors5, # 邻域数量越大越严格 minSize(80, 80) # 太小的脸不检测降低误检 ) return faces def detect_eyes(gray, face): x, y, w, h face # 眼睛通常在上半脸裁掉下半部分减少鼻孔嘴巴误检 upper gray[y:y h // 2, x:x w] eyes eye_cascade.detectMultiScale( upper, scaleFactor1.1, minNeighbors8, minSize(20, 20) ) return eyesscaleFactor1.1是精度和速度的折中调到 1.05 更细但更慢调到 1.3 快但小脸容易丢。minNeighbors对人脸取 5 左右对眼睛取 8 更严格因为眼睛区域小、纹理复杂宽松了会把眉毛、镜框当眼睛。minSize必须结合分辨率设640×480 下 80×80 的脸已经比较远再小就不可靠。detect_eyes里只取上半脸是实用技巧能明显减少误检但如果人脸框本身偏下眼睛可能被裁掉所以人脸框的稳定性比眼睛参数更重要。3.2 眼睛关键点与 EAR 计算如果资源里带关键点模型EAR 用 6 个点算更稳。左眼取水平两点和上下四点右眼同理公式是上下距离之和除以水平距离的两倍。没有关键点模型时也可以用眼睛矩形框的宽高比做近似但抗遮挡差一些。import numpy as np def eye_aspect_ratio(eye_points): # eye_points 顺序左角、上左、上右、右角、下右、下左 p1, p2, p3, p4, p5, p6 eye_points vertical np.linalg.norm(p2 - p6) np.linalg.norm(p3 - p5) horizontal np.linalg.norm(p1 - p4) if horizontal 0: return 0.0 return vertical / (2.0 * horizontal) def compute_ear_from_landmarks(landmarks, left_idx, right_idx): left np.array([landmarks[i] for i in left_idx], dtypenp.float32) right np.array([landmarks[i] for i in right_idx], dtypenp.float32) return (eye_aspect_ratio(left) eye_aspect_ratio(right)) / 2.0eye_aspect_ratio里horizontal 0的判空不能省关键点模型在极端侧脸时可能给出重合点直接除会出inf状态机就乱了。左右眼取平均能降低单眼遮挡的影响但如果你要做单眼闭眼检测就分别保留两个值。常见阈值在 0.2 到 0.3 之间具体要看摄像头距离和关键点模型不能照抄。矩形框近似版本可以写成h / w但阈值完全不同不要混用两套参数。3.3 眨眼状态机从单帧 EAR 到连续动作判定状态机要解决三件事低于阈值多久算闭眼连续多少帧算稳定一次闭眼后多久内不重复计数。下面这个类把“眨眼”定义成“闭眼后重新睁开”闭眼时长用时间戳累计不依赖帧率。import time class BlinkState: def __init__(self, ear_threshold0.22, close_frames2, cooldown0.25): self.ear_threshold ear_threshold self.close_frames close_frames self.cooldown cooldown self.closed_count 0 self.blink_count 0 self.closed_start None self.last_blink_time 0.0 def update(self, ear): now time.time() if ear self.ear_threshold: self.closed_count 1 if self.closed_start is None: self.closed_start now else: if self.closed_count self.close_frames: if now - self.last_blink_time self.cooldown: self.blink_count 1 self.last_blink_time now self.closed_count 0 self.closed_start None closed_duration 0.0 if self.closed_start is not None: closed_duration now - self.closed_start return { ear: ear, blink_count: self.blink_count, closed_duration: closed_duration, is_closed: self.closed_start is not None }ear_threshold是核心阈值close_frames2表示连续两帧低于阈值才认可闭眼能过滤单帧抖动。cooldown0.25秒是冷却时间防止一次眨眼在阈值边缘反复触发。closed_duration用time.time()算不用帧数乘 FPS因为摄像头实际帧率会掉用帧数会把闭眼时间算短。返回字典里同时给blink_count和is_closed展示层和报警层各取所需。注意last_blink_time初始为 0第一帧不会误触发。3.4 闭眼检测与实时叠加显示展示层不只是画框它决定了你调试时能不能一眼看出问题。EAR 数值、当前状态、闭眼时长、FPS 都叠在画面上调阈值时不用猜。import cv2 import time def draw_result(frame, face, state, fps): x, y, w, h face color (0, 0, 255) if state[is_closed] else (0, 255, 0) cv2.rectangle(frame, (x, y), (x w, y h), color, 2) text1 fEAR: {state[ear]:.3f} text2 fBlink: {state[blink_count]} text3 fClosed: {state[closed_duration]:.2f}s cv2.putText(frame, text1, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) cv2.putText(frame, text2, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) cv2.putText(frame, text3, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) cv2.putText(frame, fFPS: {fps:.1f}, (10, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 0), 2)color根据闭眼状态切换调试时红绿变化比看数字快。FPS建议用滑动平均不要每帧用1 / (now - last)直接打印波动会很大。closed_duration保留两位小数足够实时预览不需要更高精度。绘制文字位置固定在左上角避免跟随人脸框跳动导致看不清。真正部署时可以把这些调试信息收进开关但开发阶段不要省黑匣子式调试会浪费大量时间。4. 跑起来环境安装、摄像头调试与命令行参数代码看懂了下一步是让它在你的机器上出画面。实时项目最容易卡在环境而不是算法摄像头权限、OpenCV 版本、模型路径三件事先确认。下面按“装依赖、看入口、换输入、做自检”的顺序走每一步都有可复现的命令和失败时该看的地方。4.1 依赖安装与版本约束建议用虚拟环境不要和系统 Python 混装。核心依赖只有 OpenCV 和 NumPy关键点方案再加 dlib 或对应模型运行时。python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install opencv-python numpy # 如果教程使用关键点模型再装对应依赖 # pip install dlibopencv-python装 4.x 即可常见接口CascadeClassifier、VideoCapture、detectMultiScale都稳定。装完先跑python -c import cv2; print(cv2.__version__)能打印版本说明基础环境通了。dlib 在部分平台需要 CMake 和编译器装不上时先别死磕确认项目是否真的需要关键点如果源码只用 Haar跳过 dlib 也能跑。模型 XML 文件要放在代码能找到的相对路径下不要假设它在 OpenCV 安装目录里路径错的表现就是启动时报空级联。4.2 主程序入口与参数说明入口用argparse接参数比在代码里改常量方便。下面这段覆盖摄像头索引、视频路径、阈值和显示开关实际项目可以按需增删。import argparse def parse_args(): parser argparse.ArgumentParser() parser.add_argument(--source, default0, help摄像头索引或视频文件路径) parser.add_argument(--ear-threshold, typefloat, default0.22, helpEAR 低于该值视为闭眼) parser.add_argument(--close-frames, typeint, default2, help连续闭眼帧数过滤单帧抖动) parser.add_argument(--width, typeint, default640) parser.add_argument(--height, typeint, default480) parser.add_argument(--no-show, actionstore_true, help不显示窗口用于后台压测) return parser.parse_args()--source默认0是本机默认摄像头传视频路径就能离线跑。--ear-threshold和--close-frames暴露出来调参不用改源码。--no-show在批量跑视频时有用避免没有显示器环境报错。--width和--height只是请求值摄像头不一定完全支持读帧后要用frame.shape确认真实分辨率。参数名尽量和状态机字段对应后面写日志时不容易混。4.3 摄像头与视频文件两种输入把输入统一成VideoCapture摄像头和视频文件走同一条循环减少分支。import cv2 def open_source(source, width, height): if source.isdigit(): cap cv2.VideoCapture(int(source)) cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) else: cap cv2.VideoCapture(source) if not cap.isOpened(): raise RuntimeError(f无法打开输入源: {source}) return cap cap open_source(0, 640, 480) while True: ok, frame cap.read() if not ok: break # 后续检测与绘制source.isdigit()区分摄像头索引和文件路径传1就试第二个摄像头。cap.isOpened()只说明打开成功不代表每帧都能读到所以循环里还要判ok。摄像头读帧失败常见原因是权限、被其他程序占用、索引不对视频文件读失败常见原因是路径含中文或编码不支持先换英文短路径试。cap.set的分辨率请求可能被驱动忽略不要用请求值去算坐标始终用实际frame.shape。4.4 运行结果自检跑起来后按下面几项自检能快速定位是环境问题还是算法问题检查项正常表现异常时先看窗口是否出画面实时彩色画面摄像头索引、权限、cap.read()人脸框是否稳定正脸时框住整脸光照、minNeighbors、分辨率眼睛框是否落在眼睛上左右眼附近小框是否只裁上半脸、minSizeEAR 数值是否随眨眼变化睁眼高、闭眼低关键点顺序、阈值范围眨眼计数是否合理一次眨眼加一冷却时间、连续帧数提示先保证cap.read()连续返回 True再调检测参数读帧都不稳后面所有阈值都是玄学。如果画面正常但 EAR 一直不变先打印眼睛 ROI 的尺寸尺寸为 0 说明眼睛没检到如果眨眼计数疯涨先把close_frames调到 3 到 4再把cooldown加到 0.3 秒看是否收敛。自检顺序不要反先输入后算法能少走很多弯路。5. 避坑与排查摄像头、光照、误检和性能的 5 个血泪经验实时人眼识别项目翻车很多时候不是 EAR 公式错而是输入不稳、阈值太理想、状态机没防抖。下面 5 条是我在课程演示和原型里反复遇到的按“现象、原因、解决”拆开写你遇到类似表现可以直接对照。5.1 现象摄像头能打开但窗口全黑现象程序不报错cap.isOpened()返回 True但imshow出来的窗口全黑或者等很久才出一帧。原因通常不是 OpenCV 坏了而是读帧失败被忽略、摄像头被其他程序占用、索引指向了虚拟设备或者驱动在低分辨率下才工作。解决时先把ok, frame cap.read()的ok打出来连续打印 10 帧如果大量 False就换索引 0、1、2 逐个试再检查系统隐私设置里摄像头权限最后把分辨率降到 320×240 试有些采集设备在高分辨率下协商失败。循环里不要写frame为空还继续cvtColor那会把问题变成异常反而看不出是读帧失败。5.2 现象眨眼计数疯狂上涨现象人没怎么眨眼blink_count每秒涨好几次。原因一般是 EAR 阈值设得太高睁眼数值在阈值附近抖动或者没有连续帧过滤单帧低于阈值就计数再或者一次闭眼后没有冷却时间睁开瞬间反复触发。解决时先看画面上的 EAR 数值记录睁眼和闭眼的大致范围把阈值放到两者中间偏下再把close_frames加到 2 或 3最后加 0.25 秒冷却。如果眼睛框本身在跳EAR 会跟着跳这时要先修检测框不要继续压阈值。状态机里“闭眼后重新睁开才算一次”的定义也要确认别把每帧闭眼都当眨眼。5.3 现象侧脸或戴眼镜时眼睛框乱跳现象正脸时正常一转头眼睛框跑到眉毛、镜框或脸颊上。原因是 Haar 眼睛分类器对正脸和纹理敏感侧脸时人脸框偏移眼睛 ROI 跟着偏镜片反光也会破坏纹理。解决时先限制眼睛检测区域在人脸上半部分并适当扩大人脸框再裁 ROI侧脸场景不要硬调minNeighbors越严格越容易丢考虑换关键点方案戴眼镜时避免强光直射镜片必要时用关键点而不是矩形框。还有一个小坑人脸框坐标是相对整帧的眼睛框坐标是相对 ROI 的绘制时别忘了加回偏移否则框会画到左上角。5.4 现象帧率越跑越低现象刚开始 25 帧跑几分钟掉到 10 帧以下或者一开绘制就卡。原因常见是每帧都做全图多尺度检测、绘制文字过多、视频写入没控制、分辨率太高。解决时先把检测频率降下来人脸检测每 2 到 3 帧做一次中间帧复用上一帧框再把分辨率降到 640×480 或 480×360绘制只保留必要信息FPS 用滑动平均如果开了视频保存确认写线程没有拖慢主循环。Haar 的scaleFactor调到 1.2 也能省时间但小脸会丢按场景取舍。性能问题不要靠猜先打印每帧各阶段耗时通常检测占大头。5.5 现象闭眼超过几秒才报警/不报警现象闭眼很久但closed_duration不涨或者报警条件一直不触发。原因可能是状态机只在“眨眼完成”时更新计数闭眼期间没累计时间也可能用帧数乘 FPS 算时长实际帧率掉了导致时间偏短还可能是阈值太高闭眼数值没低于阈值。解决时用time.time()记录闭眼开始时间每帧算差值把is_closed和closed_duration分开输出先确认状态是否进入闭眼报警阈值用秒不用帧。如果你要区分“正常眨眼”和“长时间闭眼”在状态机里加一个long_close_threshold超过就置报警标志不要和眨眼计数混在一个变量里。6. 二次开发换检测器、加报警和阈值自适应的改造清单这份源码最好的用法是当基线不是当终点。换检测器时保留detect_faces和compute_ear两个接口状态机不动加报警时把closed_duration接到声音或日志不要改判定核心调阈值时先录一段视频离线回放比对着摄像头盲调快得多。下面这张表是我加日志时常用的字段直接对应状态机输出后面做统计不用再回头改。字段含义来源timestamp当前帧时间time.time()ear双眼平均 EARcompute_earis_closed是否闭眼状态机closed_duration连续闭眼秒数状态机blink_count累计眨眼次数状态机fps当前帧率滑动平均改造时先做小步验证换检测器后只跑 100 帧对比人脸框和 EAR 曲线加报警后先用视频文件跑确认不会每帧都响阈值自适应不要一上来就上复杂算法先按场景分两档室内一档、室外一档够用再迭代。从那以后我每次接摄像头项目都强制先把采集、检测、判定三层拆开跑一遍确认read不返回空、框不飘、EAR 有变化再谈阈值和报警。希望帮到你。本文还有配套的精品资源点击获取