☰
Python+OpenCV实时人眼识别与眨眼检测:基于EAR的疲劳驾驶预警实战
2026/10/16 6:42:24 网站建设 项目流程

简介:这份资源面向计算机视觉初学者与需要落地人脸相关功能的开发者,围绕Python与OpenCV实现实时人眼识别、眨眼检测与闭眼检测,并配套完整教程,帮助读者从零搭建可运行的眼部状态分析流程。压缩包共61个文件,约75.34MB,以37个py源码脚本为核心,辅以png、jpg示例图片、dat人脸关键点模型、pdf教程文档及md说明等,覆盖从环境配置到代码调试的完整链路。资源在Ubuntu环境下验证,教程会讲解Haar级联分类器定位眼睛、帧间差异判断眨眼、持续闭合状态识别闭眼等关键思路,并给出依赖安装与运行方法。目前已有4351人学习下载,适合想快速掌握OpenCV人脸特征检测、积累计算机视觉实践案例的读者参考复用。

1. 从一次疲劳驾驶预警说起:这套 Python+OpenCV 方案到底能干什么

前阵子帮朋友看一个车载疲劳监测的小需求,核心诉求很朴素:摄像头对着司机,实时判断有没有频繁眨眼或者闭眼时间过长。翻了一圈现成方案,要么依赖商业 SDK,要么模型重得跑不动。最后落到一套基于 Python + OpenCV 的轻量实现上,也就是这份「实时人眼识别 + 眨眼检测 + 闭眼检测」的源代码加教程。它不训练大模型,靠的是人脸关键点定位加眼睛纵横比(EAR)这套经典几何方法,一台普通笔记本的摄像头就能实时跑起来。

这套东西适合谁?想入门计算机视觉、又不想一上来就啃深度学习框架的开发者;做疲劳驾驶预警、在线课堂专注度检测、护眼提醒这类小功能的同学;还有需要快速验证一个视觉想法、不想被环境配置拖住的人。它把「人脸检测 → 眼睛定位 → 眨眼判定 → 闭眼报警」这条链路完整走通,代码量不大,但每一环都能单独拆出来改。下面我就按「能干什么 → 怎么跑 → 怎么改」的顺序,把这份源码拆开讲一遍。

2. 技术选型与整体架构:为什么是 dlib 关键点而不是深度学习

2.1 这套方案的技术栈构成

先明确这份源码用到的几个核心依赖,都是 Python 视觉圈里最常规的组合:

组件作用说明
OpenCV摄像头读取、图像预处理、绘制负责视频流这一层
dlib人脸检测 + 68 点关键点提供眼睛区域的坐标
NumPy向量与坐标计算算 EAR 值靠它
SciPy欧氏距离计算关键点间距
imutils图像缩放、坐标转换简化样板代码

选 dlib 而不是 MediaPipe 或自训练 CNN,原因很实际:dlib 的 68 点模型是现成的,装上就能用,不需要额外下载权重文件、不需要 GPU。代价是它对侧脸和遮挡的鲁棒性一般,但对「正脸对着摄像头」这种疲劳监测场景足够。常见做法是先用 dlib 的 HOG 人脸检测器框出人脸,再在框内做 68 点回归,拿到左右眼各 6 个关键点。

2.2 眨眼检测的核心原理:EAR 眼睛纵横比

整套逻辑的灵魂是一个叫 EAR(Eye Aspect Ratio,眼睛纵横比)的指标。眼睛睁开时,上下眼睑距离大;闭上时,这个距离趋近于零。EAR 就是拿眼睛的「高度」除以「宽度」:

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

其中 p1 到 p6 是单只眼睛的 6 个关键点,p1、p4 是左右眼角,p2、p6 和 p3、p5 分别是上下眼睑的两组点。睁眼时 EAR 大概在 0.25 到 0.3 之间,闭眼时会掉到 0.2 以下。源码里就是靠这个阈值加连续帧计数来判断「这是一次眨眼」还是「这是闭眼没睁开」。

2.3 从摄像头到报警的完整数据流

把链路串起来看,一个视频帧进来之后经历这么几步:读取帧 → 灰度化 → dlib 检测人脸矩形 → 在矩形内预测 68 点 → 从 68 点里切出左右眼索引 → 分别算两只眼的 EAR → 取平均 → 和阈值比较 → 维护一个连续闭眼帧计数器 → 超过阈值就触发报警。这个流程每一帧都跑一遍,所以性能瓶颈主要在 dlib 的关键点预测上。源码里用 imutils 把帧缩放到宽度 450 左右再处理,就是为了压住这个开销。

3. 环境搭建与跑通第一帧:从装依赖到摄像头出画面

3.1 依赖安装与 dlib 编译避坑

先把环境铺好。Python 建议 3.7 到 3.10,太新的版本 dlib 预编译包可能对不上。

# 建议先建虚拟环境,避免污染全局 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 核心依赖 pip install opencv-python pip install dlib pip install imutils pip install scipy pip install numpy

dlib 是这里最容易翻车的一环。它在 Windows 上如果没有预编译 wheel,会尝试本地编译,需要 CMake 和 C++ 编译器,很多人卡在这一步。常见做法是直接装带编译好的版本,或者用 conda 装conda install -c conda-forge dlib,能省掉一堆编译问题。Linux 上一般pip install dlib就能过,但机器上要有cmake和build-essential。

提示:如果import dlib报找不到模块,先确认虚拟环境是否激活,再确认 pip 装到了当前解释器对应的环境里,用pip -V看路径。

3.2 关键点模型文件与目录结构

dlib 的 68 点预测需要一个模型文件,通常叫shape_predictor_68_face_landmarks.dat,几十兆大小,源码包里一般会附带或者给出获取方式。把它放在项目根目录,代码里按相对路径加载。一个典型的目录结构长这样:

eye-blink-detection/ ├── shape_predictor_68_face_landmarks.dat # 关键点模型 ├── blink_detect.py # 主程序 ├── ear_utils.py # EAR 计算与关键点索引 └── requirements.txt # 依赖清单

主程序负责视频循环,ear_utils.py把眼睛关键点索引和 EAR 计算单独抽出来,方便复用和测试。这种拆法在二次开发时很关键——你想换检测逻辑,只动工具模块就行,不用碰主循环。

3.3 主循环代码逐段拆解

先看关键点索引和 EAR 计算这部分,这是整套逻辑的地基:

import numpy as np from scipy.spatial import distance as dist # dlib 68 点里,左眼和右眼各 6 个点的索引 LEFT_EYE_IDX = list(range(42, 48)) RIGHT_EYE_IDX = list(range(36, 42)) def eye_aspect_ratio(eye_points): """输入单只眼睛的 6 个 (x, y) 坐标,返回 EAR 值""" # 垂直方向两组距离:上眼睑到下眼睑 A = dist.euclidean(eye_points[1], eye_points[5]) B = dist.euclidean(eye_points[2], eye_points[4]) # 水平方向距离:左右眼角 C = dist.euclidean(eye_points[0], eye_points[3]) # 纵横比公式,乘 2 是为了归一化两组垂直距离 ear = (A + B) / (2.0 * C) return ear

这段是整个项目的核心。LEFT_EYE_IDX和RIGHT_EYE_IDX是 dlib 68 点模型的固定约定,42 到 47 是左眼,36 到 41 是右眼,顺序不能乱。eye_aspect_ratio里 A、B 是上下眼睑的两组垂直距离,C 是眼角水平距离,最后(A+B)/(2C)得到比值。睁眼时这个值稳定在 0.3 附近,闭眼时迅速下降。参数上唯一要调的就是后面比较用的阈值,一般取 0.2 到 0.25 之间。

再看主循环怎么把它用起来:

import cv2 import dlib from imutils import face_utils from ear_utils import LEFT_EYE_IDX, RIGHT_EYE_IDX, eye_aspect_ratio # 阈值与连续帧数:这两个参数决定灵敏度 EAR_THRESHOLD = 0.25 # 低于这个值判定为闭眼 CONSEC_FRAMES = 3 # 连续多少帧闭眼才算一次眨眼 detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") cap = cv2.VideoCapture(0) counter = 0 # 连续闭眼帧计数 total_blinks = 0 # 累计眨眼次数 while True: ret, frame = cap.read() if not ret: break frame = cv2.resize(frame, (450, int(frame.shape[0] * 450 / frame.shape[1]))) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 0) for face in faces: shape = predictor(gray, face) shape = face_utils.shape_to_np(shape) # 转成 NumPy 数组方便切片 left_eye = shape[LEFT_EYE_IDX] right_eye = shape[RIGHT_EYE_IDX] # 两只眼取平均,抗单眼误差 ear = (eye_aspect_ratio(left_eye) + eye_aspect_ratio(right_eye)) / 2.0 if ear < EAR_THRESHOLD: counter += 1 else: # 闭眼后又睁开,且持续帧数够,记一次眨眼 if counter >= CONSEC_FRAMES: total_blinks += 1 counter = 0 cv2.putText(frame, f"Blinks: {total_blinks}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imshow("Eye Blink Detection", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

主循环里几个点值得说清楚。EAR_THRESHOLD和CONSEC_FRAMES是最需要按场景调的两个参数:阈值太低会漏检闭眼,太高会把正常睁眼误判成闭眼;连续帧数太小会把眼球转动误当成眨眼,太大又会漏掉快速眨眼。counter的用法是「闭眼累加、睁眼结算」,只有闭眼持续到CONSEC_FRAMES帧以上、再睁开,才计一次有效眨眼,这样能过滤掉噪声。face_utils.shape_to_np把 dlib 的对象转成 NumPy 数组,后面才能用索引切片。帧缩放那一步是为了降负载,450 宽度是速度和精度的折中。

4. 眨眼与闭眼判定逻辑:阈值、计数器与报警怎么落地

4.1 眨眼和闭眼的区别到底在哪

很多人第一次写会混淆「眨眼」和「闭眼」。眨眼是瞬时的,闭眼是持续的。源码里区分这两者靠的是时间维度:同样是 EAR 低于阈值,如果只持续两三帧就恢复,那是一次眨眼;如果持续几十帧还没恢复,那就是闭眼,该报警了。所以判定逻辑里其实有两套计数——一套数眨眼次数,一套盯闭眼时长。

# 闭眼报警相关参数 EYE_CLOSED_ALERT_FRAMES = 20 # 连续闭眼超过这个帧数就报警 closed_counter = 0 # 闭眼持续帧计数 if ear < EAR_THRESHOLD: counter += 1 closed_counter += 1 if closed_counter >= EYE_CLOSED_ALERT_FRAMES: cv2.putText(frame, "DROWSINESS ALERT!", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) else: if counter >= CONSEC_FRAMES: total_blinks += 1 counter = 0 closed_counter = 0

EYE_CLOSED_ALERT_FRAMES这个参数直接决定报警的灵敏度。按 30 帧每秒算,20 帧大约是 0.67 秒,正常眨眼不会持续这么久,所以能区分开。实际部署时这个值要结合摄像头帧率换算,帧率不稳定的话最好改成按时间戳算,而不是按帧数。

4.2 参数调优:不同场景下阈值怎么定

阈值不是拍脑袋定的,得看你的使用场景和摄像头质量。下面这张表是我在不同条件下试出来的经验值,可以直接当起点:

场景EAR_THRESHOLDCONSEC_FRAMES闭眼报警帧数
室内正脸、光线好0.25320
戴眼镜、有反光0.22425
侧脸较多、走动中0.20530
低帧率摄像头0.252按时间换算

戴眼镜的人关键点定位容易偏,EAR 整体会偏低,所以阈值要往下调,否则正常睁眼也会被误判成闭眼。侧脸场景下 dlib 关键点抖动大,连续帧数要调高来平滑。这些参数没有万能值,最好的办法是先把 EAR 值实时打印出来,观察自己睁眼闭眼时的实际范围,再定阈值。

4.3 把报警接出去:声音、日志还是外部信号

检测到闭眼之后怎么报警,源码里一般只画个文字,实际项目里可以接更多东西。常见做法有这么几种:用playsound或系统蜂鸣器播提示音;把闭眼事件写进日志文件带时间戳,方便事后分析;通过串口或网络把信号发给外部设备。下面是一个写日志的简单扩展:

import time def log_drowsiness(log_path="drowsy.log"): """记录一次闭眼报警,带时间戳""" with open(log_path, "a", encoding="utf-8") as f: f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} drowsiness detected\n")

这个函数在报警分支里调用一次就行。注意别每帧都写,否则日志会爆炸,最好加个冷却时间,比如两次记录之间至少隔几秒。报警这块是二次开发最常改的地方,因为不同项目对「报警」的定义完全不同。

5. 常见问题排查:跑不起来、误报、卡顿的踩坑记录

5.1 摄像头打不开或画面全黑

现象是cap.read()一直返回 False,或者窗口一片黑。原因通常是摄像头被其他程序占用,或者VideoCapture(0)的索引不对。解决方法是先关掉其他用摄像头的软件,然后试着换索引,VideoCapture(1)、VideoCapture(2)都试一遍。Linux 上还可以用ls /dev/video*看设备号。如果是在远程环境里跑,本地摄像头是访问不到的,得换成本地运行或者读视频文件测试。

5.2 关键点模型加载失败

报错类似Unable to open shape_predictor_68_face_landmarks.dat。原因是模型文件路径不对或者文件没下载完整。解决方法是确认文件在代码运行的当前目录下,或者用绝对路径加载。文件大小对不上(比如只有几 KB)说明下载中断了,重新获取。这个坑很常见,尤其是从压缩包里解压出来路径多了一层目录的时候。

5.3 眨眼被反复误报或漏报

现象是明明没怎么眨眼,计数却一直涨,或者眨了好几次一次没记上。原因基本都在阈值和连续帧数上。误报多说明EAR_THRESHOLD太高,把正常睁眼判成了闭眼,往下调 0.02 到 0.03 试试;漏报多说明阈值太低或者CONSEC_FRAMES太大,快速眨眼没达到帧数就被过滤了。最靠谱的排查方式是把每帧的 EAR 值打印出来,对着自己的实际动作看数值变化,比盲调参数快得多。

5.4 帧率低、画面卡顿

现象是画面一顿一顿,延迟明显。原因是 dlib 的关键点预测在每一帧都跑,分辨率越高越慢。解决办法有几个:把帧缩放到更小的宽度,比如 320;把 dlib 的人脸检测改成隔帧检测,关键点预测跟着人脸框走;或者用dlib.cnn_face_detection_model_v1之外更轻的检测方式。实测把宽度从 450 降到 320,帧率能明显回升,代价是远处小脸可能检测不到。

5.5 多人入镜时结果混乱

现象是画面里有多个人时,眨眼计数乱跳。原因是代码默认只处理faces里的第一张脸或者把所有脸的结果混在一起。解决方法是给每张脸维护独立的计数器和状态,用一个字典按人脸位置或 ID 索引。简单场景下也可以只取面积最大的那张脸,忽略其他人。

6. 二次开发改造:从单机 Demo 到可复用模块的几个技巧

跑通之后,真正有价值的是把它改成能嵌进自己项目的样子。我一般会做三件事。第一件是把检测逻辑和显示逻辑彻底分开,ear_utils.py只负责算 EAR 和判定状态,主程序只负责读帧和画图,这样换 UI 或者换输入源都不用动核心。第二件是把参数抽成配置,阈值、帧数、报警方式全放到一个字典或配置文件里,不同场景换一套参数就行,不用改代码。

# config.py —— 把易变参数集中管理 CONFIG = { "ear_threshold": 0.25, "consec_frames": 3, "closed_alert_frames": 20, "frame_width": 450, "alert_sound": True, "log_path": "drowsy.log", }

第三件是加一个状态机,而不是用零散的计数器。眨眼、闭眼、报警其实是几个明确的状态,用状态机写出来逻辑清晰,也方便扩展成「打哈欠检测」「点头检测」这些新状态。下面是个简化版的状态封装:

class EyeState: def __init__(self, threshold, consec, alert_frames): self.threshold = threshold self.consec = consec self.alert_frames = alert_frames self.blink_counter = 0 self.closed_counter = 0 self.total_blinks = 0 self.alerting = False def update(self, ear): """每帧调用一次,返回当前状态""" if ear < self.threshold: self.blink_counter += 1 self.closed_counter += 1 self.alerting = self.closed_counter >= self.alert_frames else: if self.blink_counter >= self.consec: self.total_blinks += 1 self.blink_counter = 0 self.closed_counter = 0 self.alerting = False return self.alerting

这个类把一帧的判定收进一个方法,外部只管喂 EAR 值、拿报警状态。想加新指标,比如统计单位时间眨眼频率,在类里加个时间窗口就行,不影响主循环。验证改造是否成功,我习惯用一段录好的视频当输入,而不是实时摄像头,这样每次跑的结果可复现,调参数时能对比前后差异。

最后说个血泪经验:这类视觉项目最容易被忽略的是光照。同一套参数,白天和晚上、顺光和逆光,表现能差出一大截。我现在的习惯是,任何依赖关键点定位的功能,上线前都强制在目标环境的真实光照下录几段视频跑一遍,而不是在工位上对着屏幕调好就完事。从那以后我每次调阈值都先固定一段测试视频,改完参数重跑对比,再也不敢凭感觉拍数字了。希望这套拆解能帮你少走点弯路,把这份源码真正用起来。

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

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

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

立即咨询