☰
Python+OpenCV实时人眼检测:眨眼与闭眼识别源码解析
2026/10/16 3:32:23 网站建设 项目流程

简介:这份资源面向计算机视觉初学者与需要落地实时人脸眼部分析的开发者,围绕Python与OpenCV实现人眼识别、眨眼检测和闭眼检测,并配套完整教程,帮助读者从零搭建可运行的眼部状态监测流程。压缩包共61个文件,约75.34MB,以37个py源码文件为核心,辅以png、jpg示例图、dat人脸关键点模型、pdf教程及md说明等,覆盖代码、素材与文档三类内容,便于边看边跑。资源中已包含基于Haar级联分类器的人眼定位、帧间差异与光流等眨眼判定思路,以及闭眼状态持续检测的实现,教程还说明了Ubuntu环境下的依赖安装与运行方式。目前已有4351人学习下载,适合希望快速理解眼部检测原理、复用现成脚本并在此基础上做二次开发的读者参考。

1. 从一次疲劳驾驶预警翻车说起:这套 Python+OpenCV 人眼状态检测源码到底能干什么

去年帮一个做车载终端的团队看一个疲劳预警 Demo,需求很朴素:摄像头对着驾驶员,闭眼超过 1.5 秒就报警。他们一开始想用通用人脸检测硬扛,结果白天还行,一进隧道、戴个墨镜、侧个脸就疯狂误报,司机被吵得直接拔线。后来换成「人脸检测 + 眼睛区域定位 + 眼睛纵横比(EAR)时序判断」这条链路,误报率才压下来。这套Python+OpenCV 实时人眼识别、眨眼检测、闭眼检测源码,走的就是这条被验证过的工程路线:不追求花哨的深度学习大模型,而是用 dlib 的 68 点关键点 + EAR 阈值 + 连续帧计数,把「睁眼 / 闭眼 / 眨眼」三态判得又稳又轻。

它适合谁?一是做疲劳驾驶、在线学习专注度、护眼提醒这类实时性优先的嵌入式或桌面端项目的人;二是刚学完 OpenCV 基础、想找一个能跑通「检测→定位→状态判断→报警」完整闭环的练手项目的人;三是手里已经有摄像头硬件、需要一套可改阈值、可换模型的参考实现的人。源码本身不依赖 GPU,普通笔记本 CPU 就能跑到 25~30 FPS,这是它比很多「一上来就 YOLO 人脸」方案更接地气的地方。下面我按「先讲清原理和选型,再落到能抄的代码,最后说坑」的顺序拆一遍。

2. 人眼识别链路拆解:dlib 68 点、EAR 公式与眨眼判定逻辑

2.1 为什么选 dlib 68 点而不是纯 Haar 级联

OpenCV 自带的 Haar 级联能检测「眼睛」,但它给的是一个矩形框,你只知道「这块区域大概是眼睛」,不知道眼皮是睁是闭。而眨眼检测的核心是眼睛的开合程度,必须拿到上下眼睑的坐标点。dlib 的shape_predictor_68_face_landmarks模型会在每只眼睛上给出 6 个关键点,左眼索引 36~41,右眼索引 42~47,顺序固定:左右眼角、上眼睑两点、下眼睑两点。有了这 6 个点,才能算纵横比。

选型上,常见做法是「人脸检测用 dlib 的 HOG 检测器或 OpenCV DNN,关键点用 dlib」。HOG 检测器在 CPU 上够快,但对小脸、侧脸一般;如果项目对侧脸鲁棒性要求高,可以换 OpenCV 的 DNN 人脸检测器(ResNet SSD),再把检测框喂给 dlib 关键点模型。源码默认用 HOG,是为了让新手零配置跑起来。

2.2 EAR 公式:把「睁眼闭眼」变成一个数

眼睛纵横比(Eye Aspect Ratio)的定义很直观:眼睛竖直方向的距离之和,除以水平方向距离的两倍。

EAR = (|p2-p6| + |p3-p5|) / (2 * |p1-p4|)

其中 p1 是左眼角,p4 是右眼角,p2、p3 是上眼睑两点,p5、p6 是下眼睑两点。睁眼时上下距离大,EAR 通常在 0.25~0.35;闭眼时上下眼睑贴合,EAR 会掉到 0.15 以下。这个公式的好处是对尺度不敏感——脸离摄像头远近,分子分母同步变化,比值基本稳定,所以不用做复杂的归一化。

2.3 眨眼与闭眼的区别:靠连续帧计数

单帧 EAR 低只能说明「这一帧眼睛是闭的」,不能说明是眨眼还是持续闭眼。工程上的判定逻辑是:

  • 维护一个计数器COUNTER,当 EAR 低于阈值EAR_THRESH时累加;
  • 当 EAR 重新高于阈值时,如果COUNTER落在[2, 4]帧区间,判定为一次眨眼;
  • 如果COUNTER持续超过CLOSED_FRAMES(比如 20 帧,按 30 FPS 约 0.67 秒),判定为闭眼并触发报警。

这里有个容易忽略的点:帧率不同,同样的帧数代表的时间完全不同。所以更稳妥的做法是把帧数换算成秒,用time.time()做时间戳判断,而不是死记帧数。源码里两种写法都有,建议实际部署时改成时间制。

2.4 环境准备与依赖安装

先把环境搭起来。Python 建议 3.8~3.10,dlib 对 3.11 以上的 wheel 支持时好时坏,容易在编译上翻车。

# 创建虚拟环境,避免污染系统 Python python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装核心依赖 pip install opencv-python==4.8.1.78 pip install dlib==19.24.2 pip install imutils==0.5.4 pip install scipy==1.11.4

参数说明:opencv-python负责摄像头读取、图像绘制和 DNN 模块;dlib提供人脸检测和 68 点关键点模型;imutils是个小工具库,源码里用它做图像缩放和坐标转换,省得自己写;scipy用于计算欧氏距离。如果pip install dlib卡在编译,Windows 上常见做法是去下载对应 Python 版本的预编译 whl 文件本地安装,Linux 上先apt install cmake build-essential再装。

模型文件需要单独下载两个:shape_predictor_68_face_landmarks.dat(约 99MB)和可选的dlib_face_recognition_resnet_model_v1.dat。前者是必须的,放到项目根目录,代码里用相对路径加载。

3. 核心代码逐段拆:从摄像头取流到 EAR 计算与报警触发

3.1 关键点索引与 EAR 计算函数

先写最核心的 EAR 计算。这段代码决定了整个检测的精度,索引写错一位,结果就全乱。

import dlib import cv2 import numpy as np from scipy.spatial import distance as dist # dlib 68 点模型中左右眼的索引,顺序不能改 LEFT_EYE_IDX = list(range(36, 42)) # 左眼 6 个点 RIGHT_EYE_IDX = list(range(42, 48)) # 右眼 6 个点 def eye_aspect_ratio(eye_points): """ 计算单只眼睛的 EAR eye_points: 形状为 (6, 2) 的数组,顺序为 [左角, 上1, 上2, 右角, 下2, 下1] """ # 竖直方向:上眼睑两点到下眼睑两点的距离 vertical_a = dist.euclidean(eye_points[1], eye_points[5]) vertical_b = dist.euclidean(eye_points[2], eye_points[4]) # 水平方向:左右眼角的距离 horizontal = dist.euclidean(eye_points[0], eye_points[3]) # 防止水平距离为 0 导致除零 if horizontal == 0: return 0.0 ear = (vertical_a + vertical_b) / (2.0 * horizontal) return ear

逻辑说明:eye_points[1]和eye_points[5]是一对上下点,eye_points[2]和eye_points[4]是另一对,两对竖直距离相加再除以两倍水平距离,就是标准 EAR。参数上,dist.euclidean算的是二维欧氏距离,比手写sqrt(dx**2+dy**2)可读性好。除零保护是血泪经验——当人脸转到极限角度、关键点重合时,水平距离可能趋近 0,不加判断会直接抛异常让程序崩掉。

3.2 主循环:检测、计算、判定、绘制

主循环把摄像头帧、人脸检测、关键点预测、EAR 判定串起来。

# 初始化检测器和预测器 detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") # 判定参数 EAR_THRESH = 0.22 # EAR 低于此值认为闭眼 CONSEC_FRAMES = 3 # 连续帧数阈值,用于区分眨眼 CLOSED_SECONDS = 1.5 # 持续闭眼超过该秒数触发报警 cap = cv2.VideoCapture(0) closed_start = None # 记录闭眼起始时间 blink_count = 0 frame_counter = 0 while True: ret, frame = cap.read() if not ret: break frame = cv2.resize(frame, (640, 480)) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 0) for face in faces: landmarks = predictor(gray, face) # 把 dlib 关键点转成 numpy 坐标 points = np.array([[p.x, p.y] for p in landmarks.parts()]) left_ear = eye_aspect_ratio(points[LEFT_EYE_IDX]) right_ear = eye_aspect_ratio(points[RIGHT_EYE_IDX]) ear = (left_ear + right_ear) / 2.0 if ear < EAR_THRESH: frame_counter += 1 if closed_start is None: closed_start = cv2.getTickCount() / cv2.getTickFrequency() # 持续闭眼判断 elapsed = cv2.getTickCount() / cv2.getTickFrequency() - closed_start if elapsed >= CLOSED_SECONDS: cv2.putText(frame, "DROWSY!", (50, 100), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3) else: # 眼睛重新睁开,判断是否构成一次眨眼 if 2 <= frame_counter <= 4: blink_count += 1 frame_counter = 0 closed_start = None cv2.putText(frame, f"EAR: {ear:.2f}", (300, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.putText(frame, f"Blinks: {blink_count}", (300, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow("Eye Detection", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

逻辑说明:detector(gray, 0)的第二个参数 0 表示不上采样,速度快但小脸可能漏检;如果人脸离得远,改成 1 会提升检出率但掉帧。frame_counter在 EAR 低于阈值时累加,重新睁眼时判断是否落在 2~4 帧,这个区间对应快速眨眼;超过就归零。持续闭眼用cv2.getTickCount()做高精度计时,比time.time()更稳。参数上,EAR_THRESH=0.22是个经验值,不同人眼型差异大,戴眼镜的人建议调到 0.20,眼睛小的人可能要 0.18。

3.3 参数调优对照表

不同场景下阈值不能一套用到底,下面这张表是我在几个项目里试出来的参考区间。

参数默认值适用场景调整建议
EAR_THRESH0.22普通室内光照戴眼镜降到 0.20,强逆光升到 0.25
CONSEC_FRAMES330 FPS 摄像头帧率降到 15 时改为 2
CLOSED_SECONDS1.5疲劳驾驶预警学习专注度检测可放宽到 2.5
检测上采样0人脸占画面 1/3 以上人脸小改为 1,帧率约降 40%

提示:调参时先把 EAR 实时值打印在画面上,让测试者做睁眼、闭眼、眨眼三组动作,观察数值波动区间,再定阈值,比盲猜靠谱得多。

4. 避坑与排查:光照、眼镜、多脸和帧率这四个坑最常翻车

4.1 现象:白天正常,一进背光或暗光就疯狂误报闭眼

原因:EAR 依赖关键点定位,而 dlib 关键点模型在低对比度、逆光下会漂移,上下眼睑点挤在一起,EAR 自然偏低,被误判成闭眼。

解决:先做光照预处理。常见做法是转灰度后做 CLAHE 直方图均衡,或者用cv2.convertScaleAbs调对比度。如果场景光照变化剧烈,加一个简单的亮度检测,低于阈值时自动提高摄像头曝光或补光。代码里可以在cvtColor后加一行gray = cv2.equalizeHist(gray),代价是几毫秒。

4.2 现象:戴眼镜的人 EAR 普遍偏低,频繁触发闭眼报警

原因:镜片反光和镜框遮挡会让关键点偏移,尤其是上眼睑点被镜框「吸」过去,竖直距离被压缩。

解决:一是把EAR_THRESH下调 0.02~0.03;二是改用红外摄像头,反光问题基本消失;三是在关键点预测前先做一次眼镜区域掩膜,但实现复杂,一般项目用前两种就够。我一般会先问清楚用户戴不戴眼镜,再决定阈值。

4.3 现象:画面里有多个人时,报警逻辑乱套,眨眼计数互相干扰

原因:源码默认对faces循环里每个人独立计算,但blink_count和closed_start是全局变量,多人时状态被覆盖。

解决:把状态变量改成字典,以人脸 ID 或检测框坐标为 key。简单做法是用face对象的中心点坐标做 key,维护{key: {"counter": 0, "closed_start": None}}。如果项目只关心一个人,可以在检测后取面积最大的脸,忽略其他,这样最省事。

4.4 现象:换了个摄像头,眨眼检测变得迟钝或过于灵敏

原因:不同摄像头帧率不同,30 FPS 下 3 帧是 0.1 秒,15 FPS 下 3 帧就是 0.2 秒,眨眼判定窗口被拉长。

解决:把帧数阈值改成时间阈值。用cv2.getTickCount()记录 EAR 跌破阈值的时刻,重新睁眼时算时间差,落在 0.05~0.3 秒算眨眼。这样换摄像头不用改参数。源码里两种逻辑都保留了,实际部署建议统一走时间制。

4.5 现象:程序跑几分钟后卡顿,帧率从 30 掉到 10

原因:cv2.VideoCapture的缓冲区堆积。如果处理速度跟不上采集速度,旧帧会积压,越跑越卡。

解决:把cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)加上,让缓冲区只保留最新一帧。另外每处理完一帧可以cap.grab()丢弃多余帧。这个坑在树莓派上尤其明显,加上之后帧率能稳回来。

5. 进阶技巧:用 EAR 时序曲线做自适应阈值与状态可视化

默认固定阈值最大的问题是「一套参数打天下」,但每个人的眼型、摄像头角度都不一样。我在实际项目里更常用的是自适应阈值:程序启动后先采集 3~5 秒的睁眼数据,算出这段时间 EAR 的均值mean和标准差std,把阈值设为mean - 1.5 * std。这样不管用户眼睛大小、摄像头远近,阈值都能自动贴合。代价是启动时要求用户保持睁眼,加个倒计时提示就行。

# 自适应阈值:启动阶段采集睁眼样本 calibration = [] print("请保持睁眼 3 秒...") start = cv2.getTickCount() / cv2.getTickFrequency() while cv2.getTickCount() / cv2.getTickFrequency() - start < 3.0: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 0) for face in faces: points = np.array([[p.x, p.y] for p in predictor(gray, face).parts()]) ear = (eye_aspect_ratio(points[LEFT_EYE_IDX]) + eye_aspect_ratio(points[RIGHT_EYE_IDX])) / 2.0 calibration.append(ear) if len(calibration) > 10: mean_ear = np.mean(calibration) std_ear = np.std(calibration) EAR_THRESH = mean_ear - 1.5 * std_ear # 自适应阈值 print(f"自适应阈值: {EAR_THRESH:.3f}")

逻辑说明:采集阶段只统计睁眼帧,mean - 1.5 * std是统计学里常用的下界估计,能覆盖约 93% 的正常睁眼波动。参数上,采集时长 3 秒是折中,太短样本不足,太长用户不耐烦。如果采集到的样本少于 10 帧,说明检测没稳定,直接回退到默认 0.22。

另一个实用技巧是把 EAR 时序画出来。用一个固定长度的队列存最近 150 帧的 EAR 值,在画面角落画一条折线,阈值画一条水平线。调试时一眼就能看出阈值定得合不合适、有没有毛刺。这个可视化在给非技术用户演示时特别有用,他们能直观看到「眼睛闭上时曲线掉下去了」。

验证方法上,我一般会做三组对照:正常睁眼 10 秒看误报次数、快速眨眼 10 次看计数准不准、持续闭眼 3 秒看报警是否触发。三组都过了,才认为参数可用。如果项目要上线,还会录一段包含戴眼镜、侧脸、暗光的视频回放测试,比真人反复坐那儿靠谱。

从那以后我每次拿到这类视觉检测项目,都强制先跑一遍「光照 + 眼镜 + 多人 + 换摄像头」这四组边界测试,再谈调参。这套源码把链路搭得很完整,但阈值和状态管理一定要按自己的场景重调,直接照搬默认值大概率会在真实环境里翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询