一、承上启下
上篇算好了 EAR、装好了两个模型,本篇贴出疲劳检测.py 剩下的完整代码——主循环,一字未删,并重点讲清楚"为什么不会把正常眨眼误报成疲劳"。
二、完整代码(下):主循环
块 1️⃣ 读帧
python
while True:
ret, frame = cap.read()
if not ret:
break
- while True:无限循环,直到 break
- cap.read():读一帧,返回 (ret, frame)
- ret 为 False:摄像头断开/读取失败 → break 退出
- frame:当前帧图像(BGR)
和年龄检测篇写法一致。区别:这里 frame 后面还要经过灰度转换、检测、画轮廓、写中文,链路更长。
踩坑点:不检查 ret 就用 frame,摄像头断了会拿 None 去做后续操作,报错位置很靠后,难排查。
块 2️⃣ 灰度 + 人脸检测
python
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = detector(gray, 0)
2.1 为什么要转灰度
| 通道数 | 3 | 1 |
| 计算量 | 大 | 小 |
| HOG 检测器 | 不用颜色信息 | 只吃灰度 |
- HOG 特征只看梯度方向,不需要颜色
- 转灰度省 2/3 数据量,速度快一截
2.2 detector(gray, 0) 的参数
-
第 1 个参数:图像(灰度)
-
第 2 个参数:上采样次数
-
0:不放大,速度快,小脸可能漏检
-
1:放大 1 倍,小脸检出率高,但慢 4 倍
-
摄像头场景一般用 0 够用
-
2.3 一个重要的细节:检测在灰度上,标点用原图
python
for face in faces:
shape = predictor(frame, face) # ← 这里传的是 frame,不是 gray
-
人脸检测在 gray 上做(快)
-
68 关键点在 frame 上做(更准)
-
为什么关键点用原图:predictor 是训练在彩色图上的,给它彩色图定位更精细
-
两者坐标一致(灰度图和彩色图尺寸相同),所以 face 矩形框可以直接用在 frame 上
踩坑点:把 predictor(gray, face) 写错不会报错,但关键点精度可能下降。
块 3️⃣ 防误报第一步:无人脸就清零
python
if len(faces) == 0:
counter = 0
3.1 作用
- 画面里没有人脸 → 之前的"连续闭眼"记录作废
- 人都不在了,counter 还留着旧值不合理
3.2 为什么放在 for 循环外面
- len(faces) == 0 是"整帧级别"的判断
- 如果放进 for face in faces 里,faces 为空时循环根本不执行,这行也白写
3.3 和后面 counter 的关系
counter 是模块级全局变量(上篇定义的),这里主循环里直接读写。
-
因为在模块顶层(不在函数里),所以不需要 global 声明
-
如果以后把它包进 main() 函数,就要写 global counter
踩坑点:counter 定义在模块顶层,主循环也在顶层,所以能直接改。一旦包进函数,忘记 global 就会在 counter += 1 时报 UnboundLocalError。
块 4️⃣ 标关键点 + 提取双眼 + 算 EAR
python
for face in faces:
shape = predictor(frame, face)
shape = np.array([[p.x, p.y] for p in shape.parts()])
rightEye = shape[36:42]
leftEye = shape[42:48]
rightEAR = eye_aspect_ratio(rightEye)
leftEAR = eye_aspect_ratio(leftEye)
ear = (rightEAR + leftEAR) / 2.0
4.1 predictor(frame, face)
-
输入:图像 + 人脸矩形框
-
输出:full_object_detection 对象,含 68 个点
-
shape.parts():返回 68 个点对象的迭代器
4.2 列表推导式转坐标数组
python
shape = np.array([[p.x, p.y] for p in shape.parts()])
-
每个 p 有 .x、.y 属性
-
列表推导式生成 [[x0,y0], [x1,y1], …]
-
np.array(…) 转成形状 (68, 2) 的数组
-
必须转数组,后面 eye[1] – eye[5] 才能做向量减法
踩坑点:shape 变量被复用了——先是 dlib 对象,后是 numpy 数组。同名覆盖,后面只能拿数组用,别想再访问 dlib 对象的属性。
4.3 切片取双眼
| shape[36:42] | 右眼(画面左侧) | 36~41,6 个点 |
| shape[42:48] | 左眼(画面右侧) | 42~47,6 个点 |
- Python 切片左闭右开,36:42 取 36,37,38,39,40,41
- 命名是"人脸本人的左右眼",和画面左右相反——对着摄像头时,你的右眼在画面左边
- 代码里 rightEye = 点 36~41 = 画面左侧那只眼
4.4 双眼 EAR 取平均
python
ear = (rightEAR + leftEAR) / 2.0
-
单眼可能因为侧头、遮挡而不准
-
双眼平均更稳定,减少误判
-
这是常见做法,不是必须,但推荐
踩坑点:
-
如果 eye_aspect_ratio 里没有 .astype(np.float64),eye[1] – eye[5] 在 int 数组上也能算,但极端情况下要小心精度
-
np.linalg.norm 要求输入是数值数组,list 不行
块 5️⃣ 防误报第二步:连续帧计数(核心)
python
if ear < 0.3:
counter += 1
if counter >= 50:
frame = cv2AddChineseText(frame, "!!!危险!!!", (250, 250))
else:
counter = 0
5.1 为什么不能"一闭眼就报警"
| 正常眨眼 | 1~2 帧(约 0.03~0.06 秒) | 疯狂误报 |
| 真困闭眼 | 数十帧以上 | 该报 |
如果只看单帧 EAR,眨眼瞬间 EAR 就掉下去了,会报警 → 狼来了。
5.2 counter 的工作机制
text
连续闭眼帧数:
睁眼 → counter = 0
闭眼第 1 帧 → counter = 1
闭眼第 2 帧 → counter = 2
…
闭眼第 50 帧 → counter = 50 ≥ 50 → 报警
睁眼 → counter = 0(清零,重新计数)
-
闭眼累加:counter += 1
-
睁眼清零:else: counter = 0
-
到阈值报警:counter >= 50
5.3 为什么是 50 帧
-
摄像头一般 30 FPS
-
50 帧 ÷ 30 FPS ≈ 1.6 秒
-
正常眨眼 < 0.3 秒,不会达到
-
真困闭眼往往 > 2 秒,会达到
-
50 是经验值,可按需调(20~60 都合理)
5.4 报警方式
python
frame = cv2AddChineseText(frame, "!!!危险!!!", (250, 250))
-
位置 (250, 250):画面中央偏上
-
没有 max(0, …) 保护,因为 250 是固定值,不会是负数
-
字色用默认绿色 (0,255,0)——教程正文说"红色",但代码默认是绿色,这是原文不一致的地方,想真红色要传 textColor=(255, 0, 0)
踩坑点:
-
counter 是全局变量,模块顶层能直接改;包进函数要 global
-
阈值 0.3 和 50 都是魔法数字,可以提到文件顶部当常量
-
想让它真正"红色",得写成 cv2AddChineseText(frame, "!!!危险!!!", (250, 250), textColor=(255, 0, 0))
块 6️⃣ 画眼睛轮廓 + 显示 EAR
python
drawEye(frame, rightEye)
drawEye(frame, leftEye)
info = "EAR:{:.2f}".format(ear)
frame = cv2AddChineseText(frame, info, (0, 30))
6.1 画绿色轮廓
-
drawEye 是上篇定义的函数:凸包 + drawContours 绿色线
-
调用两次,左右眼各画一次
-
视觉反馈:闭眼时轮廓会明显变扁
6.2 格式化 EAR 字符串
python
info = "EAR:{:.2f}".format(ear)
-
{:.2f}:浮点数保留 2 位小数
-
例:EAR:0.28
-
也可以 f-string:f"EAR:{ear:.2f}"
6.3 写到左上角
python
frame = cv2AddChineseText(frame, info, (0, 30))
-
位置 (0, 30):左上角,y=30 是为了不贴着边缘
-
必须接返回值:函数返回新数组,不接等于没写
-
在 for 循环内,所以每张脸都会写一次——多张脸时后写的会覆盖前面的。单人场景没问题
踩坑点:
-
忘接返回值 → EAR 数字不显示
-
多张脸时所有 EAR 都写在 (0, 30),会互相覆盖(如果做多人版,要把 y 随脸位置偏移)
块 7️⃣ 显示 + 退出
python
cv2.imshow("frame", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
7.1 imshow
-
显示当前帧,窗口名 "frame"
-
每循环调一次,形成视频流
7.2 waitKey(1)
-
每帧等 1 毫秒
-
返回值:按键 ASCII,没按键返回 -1
-
必须有,否则窗口不刷新 / 卡死
7.3 & 0xFF 和 ord("q")
-
& 0xFF:只保留低 8 位,兼容不同系统(某些系统按键码 > 255)
-
ord("q"):字符 'q' 的 ASCII(113)
-
按 q 退出
踩坑点:
-
waitKey(0) 会卡在第一帧
-
imshow 在 for 循环外(只显示一次),这是对的;放里面会重复显示浪费
块 8️⃣ 收尾
python
cap.release()
cv2.destroyAllWindows()
-
cap.release():释放摄像头,否则其他程序打不开
-
cv2.destroyAllWindows():关闭所有 OpenCV 窗口
-
这里没有 if __name__ == "__main__":,因为整段是模块级代码(和上篇一起顺序执行)
整体数据流(下篇)
text
cap.read() → frame
│
▼ BGR2GRAY
gray
│
▼ detector(gray, 0)
faces
│
├─ len(faces)==0 → counter = 0
│
▼ for face in faces
predictor(frame, face) → shape (68,2)
│
├─ rightEye = shape[36:42]
├─ leftEye = shape[42:48]
│
▼ eye_aspect_ratio × 2 → 平均
ear
│
├─ ear < 0.3 → counter += 1
│ └─ counter >= 50 → 写"!!!危险!!!"
└─ else → counter = 0
│
▼ drawEye × 2 + 写 EAR 值
frame'
│
▼ imshow + waitKey(1)
│
▼ 'q' ? break : 继续
四、运行效果
运行程序后,摄像头画面里双眼被绿色轮廓圈住,左上角实时显示 EAR 值。睁大眼睛时数值在 0.3 以上;闭眼瞬间掉到 0.2 以下;坚持闭眼约 1.6 秒,画面中央就会亮出红色的"!!!危险!!!"。
五、小结与改进思路
上下两篇合起来,完成了"检测 → 计算 → 报警"的完整闭环。这套思路还能扩展:嘴巴周围的点也能算出"嘴巴纵横比"来检测打哈欠,再加上点头检测,就是一个更实用的疲劳驾驶预警系统。
作业:把阈值 0.3 改成 0.25 和 0.35,分别试试报警是更灵敏还是更迟钝,想一想为什么。
网硕互联帮助中心


评论前必须登录!
注册