简介:一份面向Python与OpenCV初学者及计算机视觉开发者的完整工程包,聚焦实时人眼识别、眨眼检测与闭眼检测,提供在Ubuntu环境下的源代码、模型文件与图文教程。工程以OpenCV的Haar级联分类器实现人眼定位,结合人脸关键点模型辅助判断睁闭眼状态,可应用于疲劳驾驶预警、注意力监测、表情分析等场景,适合作为计算机视觉方向的入门练手项目。包内共六十一个文件,包含三十七个Python源码文件(检测主程序、预处理与辅助脚本)、十二张演示图片、四张示例图片、一个人脸关键点模型,另有PDF图文教程和Markdown说明文档,压缩包整体约七十五MB,目录结构清晰,便于按模块阅读或提取复用。目前已有四千二百九十一人学习浏览,资源附有详细的运行与编译指导,能够帮助读者快速跑通实时检测Demo并理解关键原理;在此基础上可自行扩展连续帧眨眼计数、闭眼时长统计等进阶功能。
1. 项目整体思路与方案设计
1.1 为什么做这个项目
眼睛状态的实时分析在计算机视觉领域是一个非常经典且实用的切入点。我们开车打瞌睡时,眼皮会不自觉地变沉;上课走神时,目光会涣散;疲劳驾驶预警系统、网课注意力检测、甚至部分无障碍交互系统,核心都需要精确识别眼睛的开合状态。
这个项目以 Python+OpenCV 为底座,加入 dlib 人脸关键点检测模型,实现实时的人眼识别、眨眼检测与闭眼检测。你可以直接用摄像头现场跑起来,也可以把输入源换成视频文件,用于批量分析。代码量不大,但涉及人脸检测、关键点定位、几何特征计算与状态机判定这一整套流程,非常适合作为计算机视觉入门的综合实践项目。
1.2 技术方案选型与权衡
在实现人眼检测时,最直接的方案是用 OpenCV 自带的 Haar Cascade 级联分类器,里面预置了haarcascade_eye.xml眼睛检测模型,几行代码就能画出眼睛框。但这个方案有个天生缺陷:它只能输出眼睛的矩形框,无法提供眼睛轮廓的精确坐标,也就没法计算更高级的指标——比如眼睛开合程度。
所以我选择用 dlib 的 68 点人脸关键点模型。它能一次性定位人脸 68 个特征点,其中左右眼各占 6 个关键点。基于这 6 个点计算人眼纵横比(EAR,Eye Aspect Ratio),就能精确量化眼睛的开合程度,眨眼和闭眼本质上就是 EAR 值随时间变化的波形。
方案对比起来很简单:
| 方案 | 人眼定位精度 | 支持开合度计算 | 速度 | 依赖复杂度 |
|---|---|---|---|---|
| Haar Cascade 眼睛检测 | 矩形框,精度一般 | 不支持 | 快 | 低 |
| dlib 68点关键点 | 像素级轮廓坐标 | 支持 | 中 | 中(需安装dlib) |
| 深度学习关键点模型 | 像素级轮廓坐标 | 支持 | 依赖GPU | 高 |
我最终选了 dlib 68 点方案,原因是它在普通 CPU 上就能跑到实时帧率,而且 68 点模型文件(约 60MB)下载一次之后就能离线使用。如果你后续想换 MediaPipe 或 OpenCV 的深度学习人脸检测器,代码框架也不用大改,只需要替换关键点提取那一段即可。
1.3 眨眼检测的原理推导
先明确一个核心概念:EAR(Eye Aspect Ratio,眼睛纵横比)。具体来说,dlib 68 点模型中,右眼关键点索引是 36~41,左眼是 42~47。以右眼为例,6 个点的分布大致是:p1(36)和 p4(39)是眼睛左右两端,p2、p3(37、38)是上眼皮,p5、p6(40、41)是下眼皮。
EAR 的计算公式:
EAR = (||p2 - p6|| + ||p3 - p5||) / (2 * ||p1 - p4||)分子是两条垂直距离之和,分母是水平距离的两倍。正常人睁眼时 EAR 大约在 0.25~0.35 之间,闭眼时垂直距离趋近于 0,EAR 会掉到 0.05 以下。这个比值非常稳定,因为它只关注眼睛自身的几何比例,所以人脸在画面中远近变化、稍微倾斜,都不会对结果造成明显干扰——这正是它比固定像素阈值检测更靠谱的原因。
2. 环境准备与基础库安装
2.1 Python 环境与 OpenCV 安装
这个项目的底子就是 Python 环境,建议直接用 Anaconda 创建独立虚拟环境,避免把系统 Python 搞乱。Python 版本建议选择 3.8~3.10,太高或太低都可能遇到预编译包缺失的问题。
安装 OpenCV 这条命令就能搞定:
pip install opencv-python这里装的是opencv-python,它已经包含cv2模块。如果你想用 OpenCV 内置的 SIFT、SURF 这些专利算法,还需要装opencv-contrib-python,但本项目用不到。
这里有个常见的坑:如果你在 PyCharm 里写import cv2报ModuleNotFoundError: No module named 'cv2',绝大多数情况是解释器选错了。在 PyCharm 右下角点解释器,选到你创建虚拟环境对应的 Python,而不是系统的全局解释器。判断解释器对不对,最简单的方法是直接在 PyCharm 的 Terminal 里执行python --version和pip list,确认python和pip指向同一个环境。
2.2 dlib 库安装与编译问题解决
dlib 是这项目的另一个关键依赖,但它不像 OpenCV 那样一条pip install dlib就一定能成功。Linux 和 macOS 上通常还好,Windows 上大概率会报错,因为它需要编译 C++ 源码,要求系统里有 CMake 和 Visual Studio Build Tools。
安装命令:
pip install dlib如果报错,先装两个底层依赖:
pip install cmake然后去官网下载并安装 Visual Studio Build Tools(勾选“使用 C++ 的桌面开发”),再重新执行pip install dlib。
提示:如果编译时间太长,也可以直接下载预编译的 dlib wheel 包(搜索
dlib whl python 3.10),用pip install 下载好的.whl安装,基本能省掉 5~10 分钟的编译等待时间。
另外一个轻量级工具库imutils也要装上,它能让视频处理代码更简洁:
pip install imutils如果懒得装这个库,也可以自己写 resize 函数,但 imutils 的imutils.resize在保留宽高比方面确实方便。
2.3 需要下载的模型文件
dlib 说完还要下载人脸关键点检测模型:shape_predictor_68_face_landmarks.dat。这个文件大概 60.7MB,可以到 dlib 的官方模型库下载,搜索shape_predictor_68_face_landmarks.bz2,解压得到 .dat 文件,放到项目目录的models子目录即可。
模型文件是二进制的,不能直接当文本打开。项目运行时会读取它加载模型,加载成功后会有几秒钟的初始化耗时,这是正常现象,并不代表程序卡住了。
3. 核心代码实现与逐段讲解
3.1 完整可运行源码
直接贴出完整代码,我加了详细注释,你可以先复制跑通,再对着后面的解析加深理解。
import cv2 import dlib import imutils from scipy.spatial import distance as dist def eye_aspect_ratio(eye): # 计算上下眼皮的垂直欧氏距离 p2_p6 = dist.euclidean(eye[1], eye[5]) p3_p5 = dist.euclidean(eye[2], eye[4]) # 计算眼睛水平欧氏距离 p1_p4 = dist.euclidean(eye[0], eye[3]) # 眼睛纵横比 ear = (p2_p6 + p3_p5) / (2.0 * p1_p4) return ear # 初始化 dlib 的人脸检测器与关键点检测器 detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("models/shape_predictor_68_face_landmarks.dat") # 关键点索引 LEFT_EYE_START = 42 LEFT_EYE_END = 48 RIGHT_EYE_START = 36 RIGHT_EYE_END = 42 # 打开摄像头 cap = cv2.VideoCapture(0) if not cap.isOpened(): print("无法打开摄像头,请检查设备是否被占用") exit() print("按 q 退出程序") while True: ret, frame = cap.read() if not ret: print("获取视频帧失败") break # 缩小帧尺寸,提高检测速度 frame = imutils.resize(frame, width=640) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 0) for face in faces: # 获取人脸区域的关键点坐标 landmarks = predictor(gray, face) # 提取左右眼坐标 left_eye = [] right_eye = [] for i in range(LEFT_EYE_START, LEFT_EYE_END): x = landmarks.part(i).x y = landmarks.part(i).y left_eye.append((x, y)) for i in range(RIGHT_EYE_START, RIGHT_EYE_END): x = landmarks.part(i).x y = landmarks.part(i).y right_eye.append((x, y)) # 计算双眼 EAR left_ear = eye_aspect_ratio(left_eye) right_ear = eye_aspect_ratio(right_eye) ear = (left_ear + right_ear) / 2.0 # 在图上画出眼睛关键点 for point in left_eye: cv2.circle(frame, point, 2, (0, 255, 0), -1) for point in right_eye: cv2.circle(frame, point, 2, (0, 255, 0), -1) # 显示 EAR 值 cv2.putText( frame, f"EAR: {ear:.2f}", (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2, ) # 阈值判断 if ear < 0.2: cv2.putText( frame, "BLINK / CLOSED", (30, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2, ) cv2.imshow("Eye Detection", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()这段代码的核心处理流程就三步:检测人脸、提取关键点、计算 EAR。你看代码里没有复杂的滑动窗口或卷积操作,本质就是几何计算。为什么能稳定工作,全靠 dlib 的 68 点关键点定位在正脸前提下非常准确。
3.2 代码逻辑逐段解析
先看eye_aspect_ratio函数。参数eye是一个包含 6 个(x, y)坐标的列表,dist.euclidean来自scipy.spatial,用来计算两点间的欧氏距离。如果你不想额外装 scipy,也可以自己写((x1-x2)**2 + (y1-y2)**2) ** 0.5,结果完全一样。
再看循环里的detector(gray, 0)。第二个参数0表示不做图像金字塔上采样,也就是不主动放大图像去找更小的人脸,速度更快但远处的小脸可能检不到。如果摄像头距离人比较远,可以改成1甚至2,能提高小脸召回率,代价是每帧处理时间增加。
关键点提取部分,landmarks.part(i).x和.y返回的就是第 i 个特征点的坐标。注意左右眼的索引范围不要写反,36 是右眼(画面中你的左边),42 是左眼。两边 EAR 求平均是为了减少单眼误差——实际测试中如果人轻微侧脸,单只眼的 EAR 波动偏大,平均之后会更平滑。
3.3 为什么阈值设置为 0.2
0.2 这个数值不是拍脑袋定的。正常睁眼时 EAR 通常在 0.25~0.35,闭眼时会掉到 0.05 以下。取中间值 0.2 作为阈值,可以保证正常睁眼时不误报,闭眼或眨眼时能及时捕捉到。但在实际场景中,这个阈值会因为摄像头高度、距离、人眼大小产生偏移。
更稳的做法是增加一个“校准阶段”:程序启动前 3 秒,让人正对摄像头正常睁眼,程序自动采集这段时间的 EAR 值,取平均值的 80% 作为阈值。这样能适配不同的人,比写死 0.2 更科学。我建议你在文章里看到 0.2 时,先理解它是一个经验起点,实际部署还要做个性化调整。
4. 眨眼检测与闭眼状态判定
4.1 眨眼检测的判定逻辑
单纯判断EAR < 0.2只能说“当前眼睛闭着”,还不能说“眨了一次眼”。眨眼是一个过程:睁开眼睛 → 闭眼 → 睁开眼睛。正常人每次眨眼持续约 100~150ms,在 30FPS 的摄像头下大约是 3~5 帧。
所以,道的眨眼检测需要状态机:
- 当前帧 EAR 低于阈值,且之前是睁眼状态,则标记“可能正在眨眼”。
- 继续检测后续帧,如果看到 EAR 回到阈值以上,确认这是一次眨眼,计数器加 1。
- 如果 EAR 持续低于阈值超过一定时间,就不再是眨眼,而是闭眼状态。
这个方法在疲劳驾驶检测领域有一个更专业的名字:PERCLOS(Percentage of Eyelid Closure over the Pupil over Time),即单位时间内眼睛闭合时间所占的比例。PERCLOS 超过 0.4 通常被认为是疲劳的标志。你可以在此基础上延伸,用累计眨眼次数和闭眼时长综合评分,判断疲劳程度。
4.2 闭眼检测与疲劳预警
闭眼检测和眨眼检测不同之处在于“持续时间”。一次普通的眨眼只有几十毫秒到一百多毫秒,如果是疲劳导致的眼睛闭合,持续时间往往超过 500ms,甚至长达数秒。
代码升级思路:
- 维护一个
closed_frames计数器,连续帧 EAR 低于阈值就加 1,高于阈值就清零。 - 摄像头帧率假设为 30FPS,那么 15 帧以上的连续闭眼就代表闭眼时间超过 0.5 秒,可以触发提醒。
- 提醒方式可以是画面上的文字提示,也可以加声音告警,或通过串口发送信号给外部设备。
4.3 升级版代码框架
我在完整代码里加上眨眼计数和闭眼时长判断:
# 初始化状态变量 EAR_THRESH = 0.2 EAR_CONSEC_FRAMES = 2 # 连续 2 帧低于阈值才认为可能眨眼 CLOSED_FRAME_LIMIT = 15 # 连续 15 帧低于阈值判定为闭眼 frame_counter = 0 blink_counter = 0 eye_closed = False while True: # ... 前面的检测代码省略 ... if ear < EAR_THRESH: frame_counter += 1 else: if frame_counter >= EAR_CONSEC_FRAMES: blink_counter += 1 frame_counter = 0 if frame_counter >= CLOSED_FRAME_LIMIT: eye_closed = True else: eye_closed = False # 在画面上输出 cv2.putText(frame, f"Blinks: {blink_counter}", (30, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 0), 2) if eye_closed: cv2.putText(frame, "DROWSINESS ALERT!", (30, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)注意这里frame_counter >= EAR_CONSEC_FRAMES的判定逻辑:眨眼时眼睛闭合至少持续 2 帧,单帧 EAR 抖动导致的误检就不会被当作眨眼。这个思路和按键去抖动是同一个道理。
5. 常见问题与调试经验
5.1 dlib 安装失败的多种解法
几乎没有哪个初学者能一次装好 dlib,报错种类也是花样百出。
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
CMake must be installed | 缺少 CMake | pip install cmake |
Error: Visual Studio not found | 缺少 C++ 编译工具链 | 安装 Visual Studio Build Tools,勾选“使用 C++ 的桌面开发” |
fatal error: dlib/serialize.h: No such file | 版本不兼容 | 升级 dlib 到最新版 |
ModuleNotFoundError: No module named 'dlib' | 安装没成功 | 检查 pip list,确认是否真的装上了 |
如果你用的是 Anaconda,也可以尝试conda install -c conda-forge dlib,conda 会自动把编译好的二进制包拉下来,比 pip 省事很多。
5.2 检测速度慢与卡顿优化
dlib 的人脸检测在 CPU 上是能实时跑的,但如果你用detector(gray, 1)开启图像金字塔,速度会明显下降。我实测下来,640x480 的帧尺寸、金字塔上采样为 0 时,大约能跑 20~30FPS;上采样改为 1,帧率直接掉到 15FPS 以下。
几个提速技巧:
- 缩小帧尺寸到 480 到 640 宽度即可,不需要全高清原图。
- 只对灰度图做检测,不用彩色图。
- 如果画面中只有一个人脸,可以每 2 帧做一次人脸检测,中间 1 帧用上一帧的人脸矩形区域直接提取关键点,这样能明显提速。
- 要求高的话换 MediaPipe,它对 CPU 优化更好,帧率能翻倍,但需要额外安装
mediapipe。
5.3 现实场景中的精度问题
戴眼镜的人会明显影响关键点定位,尤其是镜框反光严重时,dlib 会在镜框边缘打出奇怪的关键点。我实测下来,无框眼镜影响最小,粗黑框眼镜偶尔会让 EAR 值抖动。解决办法是:启动时做一个“无眼镜校正”,或者改用深度学习模型(如 MediaPipe FaceMesh)替代 dlib。
侧脸时 68 点模型也会失效,因为部分关键点被遮挡。这属于方法本身的局限,不是参数能调的。如果你需要做多角度检测,建议换成 FaceMesh 的 468 点模型,它对人脸姿态的鲁棒性好很多。
5.4 摄像头与光线环境调参经验
我经历了无数次摄像头测试后发现,最影响 EAR 稳定性的外部因素不是距离,而是光线角度。
顺光下 EAR 的波动范围大约在 0.03 以内;逆光和顶光环境下,人脸区域出现明显阴影和过曝,EAR 波动能到 0.06,这时候阈值 0.2 就可能出现睁眼误报。解决方法是让摄像头尽可能靠近窗户或台灯光源,避免正上方顶光照射。如果条件不允许,可以把 EAR 阈值上调到 0.22~0.25,让误报率下降,代价是轻微的闭眼状态可能检测得稍微迟钝。
还有一点要注意:摄像头帧率会直接影响闭眼时长判断的准确性。假如摄像头实际只有 15FPS,那么 15 帧对应的闭眼时长是 1 秒,而不是 0.5 秒。建议在日志中打印实际计算出的帧率,然后动态调整CLOSED_FRAME_LIMIT,公式是:CLOSED_FRAME_LIMIT = 目标检测秒数 * 实际FPS。
6. 实际运行效果与经验总结
跑通这个项目之后,我对计算机视觉项目的认知又有了一层深化:真正实用的人眼识别与眨眼检测,不是靠复杂的算法堆出来的,而是靠“几何特征 + 状态判断 + 工程调参”三者的配合。
我个人的一个实测数据可以参考:在普通笔记本自带摄像头、室内光线良好、距离约 60cm 的条件下,EAR 在睁眼时稳定在 0.30 左右,闭眼时降到 0.04,两者差距非常大,所以阈值取 0.2 非常安全。戴眼镜之后,睁眼 EAR 会略微下降,大约在 0.27 左右,闭眼仍然低于 0.08,所以不会导致完全失效。
最后分享一个我在实际开发中非常喜欢的小技巧:在调试阶段,不要只在画面上输出“BLINK / CLOSED”这种状态文本,可以把ear数值实时打印到终端,或者画一个折线图。这样你就能直观看到 EAR 的波形——眨眼时的 V 字形波形、闭眼时的平台期波形,一目了然。等以后做疲劳检测时,你甚至可以直接分析这个波形的形态学特征,识别异常眨眼模式,那又是另一个深水区了。
本文还有配套的精品资源,点击获取