云计算百科
云计算领域专业知识百科平台

Opencv-疲劳检测2闭眼报警的主循环

一、承上启下

上篇算好了 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 切片取双眼

切片眼睛dlib 点编号
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,分别试试报警是更灵敏还是更迟钝,想一想为什么。

赞(0)
未经允许不得转载:网硕互联帮助中心 » Opencv-疲劳检测2闭眼报警的主循环
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!