☰
基于MediaPipe的实时疲劳与坐姿检测系统:关键点几何计算
2026/10/11 11:45:34 网站建设 项目流程

简介:一款基于MediaPipe的摄像头实时疲劳与坐姿检测系统,面向计算机及相关专业课程设计、综合实训或毕业设计的学生,也适合有Python基础的开发者做视觉项目参考。系统通过实时视频流分析人眼纵横比和身体关键点坐标,判断疲劳程度与坐姿规范性,发现异常立即触发警示,能起到健康提醒作用。项目曾在导师指导下作为课程设计完成,获98分评价,源码包含详细注释,核心算法与界面逻辑分离,便于阅读和二次开发。

资源包共13个文件,总大小113KB,以Python脚本为主,包含主程序、图形界面、判断模块、依赖清单、配置和说明文档等;其中4个py文件构成功能主体,yml、txt、md分别提供配置、依赖与使用说明,结构紧凑、层次清晰。

目前已有49人学习浏览。对需要完整项目实践参考的学习者,这是一份可直接运行的课设方案,从关键点检测、疲劳判断到异常提醒的完整链路均有代码支撑,配合注释和文档,能帮助理解计算机视觉在实际应用中的开发流程,适合用于课程设计演示或代码组织范本。

1. 实时疲劳与坐姿检测:一套能写进课设报告的 MediaPipe 方案

做课程设计拿到「基于 MediaPipe 与摄像头的实时疲劳与坐姿检测系统」这个题目时,第一反应千万别是「又要训练一个 YOLO」。疲劳检测的本质是眼睛闭合状态的时间累积,坐姿检测的本质是肩线和头部关键点的空间关系——这两件事都不是目标检测的活,而是关键点几何计算的活。MediaPipe 恰好同时给出 FaceMesh 的 468 点人脸拓扑和 Pose 的 33 点全身骨架,用纯 Python 加 OpenCV 就能在普通笔记本摄像头上跑到 25fps 以上,直接把课设从「调不通的深度学习」变成「可复现的算法工程」。这套方案适合正在写 Python 课设或毕设的学生,也适合想快速拿到带注释源码和高分报告做参照的从业者。下面按选型原理、环境骨架、双检测模块、踩坑记录、阈值标定的顺序完整过一遍。

2. MediaPipe 选型与检测链路:为什么是 FaceMesh + Pose,而不是 Dlib 或 YOLO

2.1 先讲清楚:疲劳和坐姿到底要检测什么

疲劳检测的核心不是「脸歪没歪」,而是双眼的上下眼睑间距在时间轴上持续压缩到某个程度;坐姿检测的核心也不是「画面里有没有人」,而是肩线相对水平线的夹角、头部中心相对肩线的偏移、头部距离摄像头的远近。这三个量全部来自关键点坐标之间的几何关系,不需要语义分割,不需要目标框。把问题拆到这个粒度,选型才不玄学:你要的是一套能稳定输出关键点坐标的模型,而不是一个能框住人的检测器。

2.2 Dlib、Haar、YOLO 对比下来,MediaPipe 赢在哪

Dlib 68 点是人脸关键点的老牌方案,但它的人脸检测是 HOG 管线,侧脸、口罩、逆光一变就丢,模型文件约 60MB,在课设源码包里显得很笨重。OpenCV 自带的 Haar 级联只给出检测框,没有关键点,根本算不出 EAR 和肩线角度,撑不起课程设计里的算法部分。YOLO 系列倒是能出框,但要做眼睛和肩部关键点还得再接姿态估计头,训练成本和推理资源对一台学生笔记本都不友好。MediaPipe 的 FaceMesh 和 Pose 都是轻量关键点模型,CPU 上单帧分别 8 到 12 毫秒,两个模型共用同一个 RGB 帧串行跑也能稳住 25fps。它还只依赖 mediapipe 和 opencv-python 两个包,不需要额外装 PyTorch 或 TensorFlow。

另一个决定性优势是眼睛区域的关键点密度。FaceMesh 单只眼睛有 6 个专用采样点,Dlib 68 点里每只眼睛只有 4 个左右,算 EAR 时需要垂直距离和水平距离的比值,采样点越密,眼睑上下沿的距离计算越稳,闭眼瞬间的数值变化越明显。疲劳检测的灵敏度就靠这一点点密度差异拉开。

2.3 FaceMesh 468 点和 Pose 33 点:关键点索引先背熟

写代码时反复查索引表会打断思路,先把用到的点列清楚。FaceMesh 是面部网格,我们只取左右眼各 6 点;Pose 是全身骨架,只取鼻子、左肩、右肩 3 点。

用途模型关键点索引角色
左眼 EARFaceMesh33, 133, 160, 158, 153, 144外眼角/内眼角/上睑外/上睑内/下睑内/下睑外
右眼 EARFaceMesh362, 263, 385, 387, 373, 380外眼角/内眼角/上睑外/上睑内/下睑内/下睑外
鼻子Pose0头部中心参考点
左肩Pose11肩线左端点
右肩Pose12肩线右端点

注意 MediaPipe 返回的 landmark 坐标都是归一化的,取值范围 0.0 到 1.0,直接做欧氏距离计算即可,不需要乘回原图宽高。这个细节容易在课设报告里被忽略,答辩时提一句反而加分。

2.4 判定逻辑为什么必须分两步:先算几何量,再套阈值

网上各种源码包里常见的地写「if 眼睛关键点之间的距离小于某个像素值就判疲劳」,这是翻车起点。不同摄像头分辨率、不同人脸到屏幕距离下,像素距离的绝对值变化非常大,同一套代码换台电脑就失效。正确做法是先构造无量纲的几何量:

  • EAR(眼睛纵横比):垂直眼睑距离除以水平眼睑距离,睁眼约 0.3,闭眼会掉到 0.15 以下
  • 肩线夹角:atan2(肩点 y 差, 肩点 x 差),正坐时接近 0 到 5 度
  • 头部偏移比:鼻尖相对肩线中点的水平差除以肩宽,正常范围在 ±0.1

这些量对画面缩放不敏感,换摄像头和分辨率只需要微调阈值,不用重写逻辑。第 4 章给出计算函数,第 6 章给出标定流程。

3. 环境搭建与摄像头主循环:跑通视频流骨架的完整步骤

3.1 环境准备:Python 版本和依赖安装

依赖只有 mediapipe、opencv-python、numpy 三个,Python 版本建议 3.9 到 3.11。用了 3.12 以上版本时 mediapipe 的 wheel 可能对不上,装完 import 阶段直接报错,这是环境里最常见的坑。建议用虚拟环境隔离,避免把系统 Python 搅乱。

python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install mediapipe opencv-python numpy

安装过程 mediapipe 会自动带下 protobuf、absl 等传递依赖,不要手动去强制升级 protobuf。一旦 protobuf 被抬到 4.x 以上,mediapipe 的 import 就会崩,而且报错信息非常迷惑,指向一个无关的 .py 文件。遇到这类问题优先检查依赖版本,而不是重装整个环境。

3.2 摄像头初始化:设备号、分辨率和帧率

cv2.VideoCapture 的设备号 0 是电脑自带摄像头,插上 USB 摄像头后设备号通常依次往后排。网络摄像头取流时直接传 rtsp:// 地址,后续代码完全不用改。网络摄像头取流要注意主码流和子码流的区别:子码流分辨率低但延迟小,做实时检测用子码流更稳,取流地址里通常带 stream 参数控制。

分辨率建议从 640x480 起步。MediaPipe 的关键点模型在低分辨率下足够稳定,1080p 输入会把 CPU 占用拉满,帧率掉到 15fps 以下,疲劳判定依赖的时间连续性就被破坏。先把 640x480 跑通,再决定要不要抬分辨率。

3.3 主循环骨架:一次读取、双模型、可扩展

import cv2 import mediapipe as mp from collections import deque mp_face_mesh = mp.solutions.face_mesh mp_pose = mp.solutions.pose face_mesh = mp_face_mesh.FaceMesh( max_num_faces=1, # 只处理一个人,省算力 refine_landmarks=True, # 开启瞳孔关键点,眼睑判定更稳 min_detection_confidence=0.5, min_tracking_confidence=0.5 ) pose = mp_pose.Pose( min_detection_confidence=0.5, min_tracking_confidence=0.5 ) cap = cv2.VideoCapture(0) if not cap.isOpened(): print("摄像头打不开,检查设备号、权限或被占用") exit(1) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) fps = 25 frame_queue = deque(maxlen=fps * 3) # 缓存最近3秒的判定结果 while cap.isOpened(): ok, frame = cap.read() if not ok: break # MediaPipe 要求 RGB 输入,OpenCV 默认 BGR rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) rgb.flags.writeable = False face_result = face_mesh.process(rgb) pose_result = pose.process(rgb) # 第4章的 EAR 与坐姿判定在此接入 # frame_queue.append((ear, tilt_angle, head_offset)) cv2.putText(frame, "press q to quit", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow("fatigue and posture monitor", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

逻辑说明:BGR 转 RGB 是 MediaPipe 的硬性要求,不转的话关键点结果会整体偏移。rgb.flags.writeable = False 告诉 numpy 这个数组是只读的,MediaPipe 内部会跳过不必要的拷贝,省一点内存带宽。两个模型共用同一个 rgb 帧串行处理,face_result 和 pose_result 是独立的 landmark 容器,后续分别取用。frame_queue 缓存最近 3 秒的判定结果,给第 4 章的迟滞判定做数据源。

参数说明:max_num_faces=1 直接砍掉多人人脸网格的计算量;refine_landmarks=True 必须开,不开就拿不到瞳孔位置,眼睛六点法的精度会下降;min_detection_confidence 控制初次检测的置信门槛,0.5 是平衡点,逆光环境可以考虑降到 0.4,但会带来轻微的关键点抖动。

提示:FaceMesh 和 Pose 共用同一个 RGB 帧,不要在循环里做两次 cvtColor,那会白白吃掉一大部分帧率。

3.4 性能基准:双模型串行到底能跑多快

裸跑这个骨架不带任何检测逻辑,在 640x480 下,i5 笔记本的 FaceMesh 单帧约 8 到 12 毫秒,Pose 单帧约 6 到 10 毫秒,加上读取和渲染,整体稳定在 25 到 30fps。如果实际帧率低于 20fps,优先检查三件事:分辨率是否设到了 1280 以上、是否有其他摄像头软件占用了设备、系统电源计划是否切到了省电模式。前三项排除后还慢,再考虑降低 min_detection_confidence 减少重检测频率。

4. 疲劳与坐姿检测实现:EAR 六点公式与肩线几何判定

4.1 疲劳检测:EAR 计算函数与连续闭眼状态机

EAR 全称 Eye Aspect Ratio,用六点法同时刻画眼睑垂直开合和水平跨度。垂直方向取两组对角线距离,水平方向取内外眼角距离,两者相除得到一个对画面缩放不敏感的比例值。睁眼时垂直距离占比高,EAR 普遍在 0.3 附近;闭眼时垂直距离趋近于零,EAR 会跌破 0.15。

import math def eye_aspect_ratio(landmarks, eye_idx): """ 计算单只眼睛的 EAR 值。 eye_idx 顺序固定为 [外眼角, 内眼角, 上睑外, 上睑内, 下睑内, 下睑外] """ def dist(i, j): return math.hypot(landmarks[i].x - landmarks[j].x, landmarks[i].y - landmarks[j].y) p1, p4 = eye_idx[0], eye_idx[1] # 外眼角、内眼角 p2, p3 = eye_idx[2], eye_idx[3] # 上睑外、上睑内 p5, p6 = eye_idx[4], eye_idx[5] # 下睑内、下睑外 return (dist(p2, p5) + dist(p3, p6)) / (2.0 * dist(p1, p4)) LEFT_EYE = [33, 133, 160, 158, 153, 144] RIGHT_EYE = [362, 263, 385, 387, 373, 380]

逻辑说明:dist 函数直接作用在归一化坐标上,不用乘图像宽高。分子是两组垂直距离之和,对应上睑外到下睑内、上睑内到下睑外,分母是内外眼角距离的两倍。这样构造保证了睁眼时分子大、分母稳定,数值能压到 0.3 附近;闭眼时分子趋近于零,数值快速掉到 0.1 级别。实际使用时取左右眼 EAR 的最小值作为当前帧的判定依据,防止单眼漏检拉高均值导致漏报。

EAR_THRESH = 0.25 CLOSE_SECONDS = 1.6 close_seconds = 0.0 fatigue = False # 主循环内部逻辑 ear = min(eye_aspect_ratio(lm, LEFT_EYE), eye_aspect_ratio(lm, RIGHT_EYE)) if ear < EAR_THRESH: close_seconds += 1.0 / fps else: close_seconds = 0.0 fatigue = close_seconds >= CLOSE_SECONDS

逻辑说明:这里用时间而不是帧数做连续闭眼判定。帧率波动时帧数阈值会失真,比如 30fps 下的 40 帧是 1.3 秒,掉到 20fps 就变成 2 秒。close_seconds 每帧累加 1/fps,一旦 EAR 回到阈值以上立刻清零,保证必须是连续闭眼而不是累积闭眼。CLOSE_SECONDS 取 1.6 秒的出发点是正常眨眼持续时间只有 0.2 到 0.4 秒,低于这个时间不触发;持续 1.5 秒以上基本就是瞌睡信号,误报率可控。

参数初始值说明
EAR_THRESH0.25低于该值判定当前帧为闭眼
CLOSE_SECONDS1.6连续闭眼超过该时长触发疲劳告警
眨眼间隔大于 2 秒闭眼频繁但每次都很短,属于眼睛半闭状态,需要单独统计

4.2 坐姿检测:肩线角度、头部偏移与前倾参考量

坐姿检测不需要额外的深度学习模型,直接用 Pose 的 11、12 号肩点算肩线角度,用 0 号鼻子点算头部偏移,用肩宽变化估算前后倾。三个量全部是无量纲几何量,对分辨率变化免疫。

def posture_check(pose_landmarks): """ 输入 Pose 模型返回的 landmarks,输出三个坐姿特征量。 返回: tilt_angle, head_offset, shoulder_width_norm """ left_shoulder = pose_landmarks[11] right_shoulder = pose_landmarks[12] nose = pose_landmarks[0] # 图像坐标系 y 轴向下,dy 的正负代表左右肩高低 dx = right_shoulder.x - left_shoulder.x dy = right_shoulder.y - left_shoulder.y # 肩线相对水平线的夹角,单位度 tilt_angle = math.degrees(math.atan2(abs(dy), abs(dx))) # 头部水平偏移:鼻尖相对肩线中点的距离,归一化到肩宽 shoulder_mid_x = (left_shoulder.x + right_shoulder.x) / 2.0 shoulder_w = max(abs(dx), 1e-5) head_offset = (nose.x - shoulder_mid_x) / shoulder_w # 前倾参考量:肩宽的归一化宽度,人离摄像头越近数值越大 shoulder_width_norm = abs(dx) return tilt_angle, head_offset, shoulder_width_norm

逻辑说明:肩线夹角的计算用 atan2 而不是简单 arctan,因为 atan2 能正确处理 dx 接近 0 的极端情况,避免除零。dy 的符号可以判断左右哪边肩高,head_offset 为正表示鼻尖偏向画面右侧。前倾参考量用归一化肩宽表示,人向前倾时肩部离摄像头更近,肩宽占比变大,与肩部基线对比能得出前倾程度。

判定逻辑按三个阈值组合:

TILT_THRESH = 15.0 # 肩线夹角超过15度判定为歪斜 HEAD_OFFSET_THRESH = 0.2 # 头部偏移超过20%肩宽判定为偏头 WIDTH_CHANGE_THRESH = 0.2 # 肩宽相对基线变化超过20%判定为前倾/后仰 bad_posture = (tilt_angle > TILT_THRESH or abs(head_offset) > HEAD_OFFSET_THRESH)

这三个阈值不是写死就完事的。摄像头装在屏幕正上方时,正坐的 tilt_angle 可能在 0 到 5 度之间;但摄像头放在侧面或高度不对时,物理上肩线本来就是斜的,直接套 15 度会一直误报。处理方法是在系统启动时记录 100 帧的肩线角度均值作为基线,后续用「当前角度减基线角度」再做判定,这个细节放第 6 章展开。

参数初始值说明
TILT_THRESH15 度左右歪斜的判定门槛
HEAD_OFFSET_THRESH0.2头部偏移比阈值
WIDTH_CHANGE_THRESH20%肩宽相对基线的变化幅度

4.3 双模块合流:把两个检测结果画到同一帧上

主循环里拿到 ear、tilt_angle、head_offset 后,用 cv2.putText 直接画到画面顶部,报警状态用红色文字叠加。课程设计演示时要让老师一眼看到「检测量数值 + 判定结果」,比只弹一个报警框更有说服力。画面中央画一条肩线,两端画圆点标注肩关节位置,能让坐姿判定的过程可视化,报告里截几张图就是现成的效果展示。

5. 避坑与排查:摄像头、阈值抖动和误报的 5 条实测记录

5.1 摄像头打不开或黑屏一片

现象:cap.isOpened() 返回 False,或者 open 成功但 read 返回的 frame 全是 None。

原因:设备号不对,插了 USB 摄像头后编号发生变化;摄像头被其他软件独占;笔记本前置摄像头权限被系统禁掉;网络摄像头取流地址写错或带认证参数缺失。

解决:先用 5 行脚本枚举设备号,从 0 试到 2,确认摄像头实际编号。权限问题到系统设置里开启摄像头访问权限。RTSP 场景先单独用 VLC 验证地址能出画面再进代码;如果是树莓派 CSI 摄像头,走的是另一套驱动节点,不属于 VideoCapture 的枚举范围,先确认 /dev/video0 节点是否生成。

5.2 眼睛关键点乱跳,EAR 忽高忽低

现象:眼睛明明睁着,EAR 在 0.15 到 0.35 之间抖动,导致频繁触发疲劳判定。

原因:FaceMesh 构造参数里 refine_landmarks 没开,拿不到精确眼睑点;min_detection_confidence 设太低导致每帧都重新检测,关键点位置不稳定;逆光或镜片反光让上眼睑点被错误吸附。

解决:确认 FaceMesh 构造参数里有 refine_landmarks=True。把 min_detection_confidence 从 0.3 调回 0.5,避免频繁重检测。补光或调整摄像头角度避免逆光。代码层面对 EAR 做中值滤波,取最近 5 帧的中位数作为当前值,能压掉大部分单帧跳变。

5.3 EAR 阈值 0.25 在别人脸上失效

现象:自己本机调好的阈值,换个人坐过来立刻连续报警或不报警。

原因:单眼皮、双眼皮、睁眼大小差异导致每个人的 EAR 基线不同。0.25 这个值参考的是标准脸型数据,没法覆盖所有人。

解决:进场时花 10 秒采集当前用户的 100 帧正坐睁眼数据,算 EAR 均值再减 0.08 作为该用户的动态阈值。比如标定出来均值 0.30,那这个人的闭眼阈值就是 0.22。这个流程写进课设报告的「系统标定」章节,答辩时是实打实的加分项。

5.4 双模型叠加后帧率掉到 10fps

现象:加了坐姿检测后画面明显卡顿,疲劳计时的 close_seconds 累加全乱。

原因:FaceMesh 和 Pose 在每个循环里串行跑,每帧开销翻倍;如果再对每帧做缩放、画框、写日志,CPU 直接被打满。

解决:确认输入分辨率只有 640x480。把 FaceMesh 和 Pose 的处理逻辑挂在同一个 cvtColor 结果上,不要各转一次 RGB。日志写入放到另一个线程,或者每 10 帧写一次,避免 I/O 阻塞主循环。

5.5 正坐被误报成歪坐:摄像头安装角度是原罪

现象:不管人怎么坐,tilt_angle 都稳定大于 15 度,系统持续报警。

原因:摄像头放在笔记本侧面或者高低角度不对,物理上画面里的肩线本来就是斜的,算法没有做基线校准。

解决:代码里加基线偏移逻辑。系统启动后前 100 帧记录肩线夹角平均值,之后用「当前角度减基线角度」做判定。同时要求摄像头在检测过程中固定角度,中途转动摄像头会让基线失效。这条算是我在坐姿检测上踩得最深的坑,血泪经验,新项目直接默认加基线校准。

6. 验证方法与阈值调优:把检测结果变成课设报告里的真实数据

6.1 动手前先做 10 秒标定

不标定就跑实时检测,阈值就是玄学。我一般把标定做成独立函数,切到标定模式后让用户正坐睁眼 100 帧,自动算出动态 EAR 阈值:

def calibrate(face_mesh, cap, n_frames=100): ears = [] for _ in range(n_frames): ok, frame = cap.read() rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) res = face_mesh.process(rgb) if res.multi_face_landmarks: lm = res.multi_face_landmarks[0].landmark ear_left = eye_aspect_ratio(lm, LEFT_EYE) ear_right = eye_aspect_ratio(lm, RIGHT_EYE) ears.append(min(ear_left, ear_right)) return sum(ears) / len(ears) - 0.08

逻辑说明:取左右眼 EAR 的最小值再参与均值计算,防止单眼漏检把均值拉高。最后减 0.08 作为安全余量,相当于把闭眼判定门槛放到睁眼基线的下方,留出眨眼和轻微波动的空间。

6.2 事件统计导出一份 CSV,报告素材直接产

检测到疲劳事件就把时间点、左右眼 EAR、肩线角度、头部偏移、是否触发告警写进 CSV。课设报告里画一条 EAR 随时间变化的折线图,再附一张事件时间表,比任何文字描述都有说服力。统计指标推荐用简化版 PERCLOS:统计每分钟闭眼帧数占比,超过 0.2 判定为疲劳状态,这个指标在学术界有据可查,报告中引用文献时站得住。

答辩时把 30 分钟录屏的 EAR 曲线和事件列表贴进 PPT,老师问阈值怎么来的,把标定流程和数据采集过程一讲,问题基本就过了。从那以后我每做一套检测系统,都会强制走一遍「100 帧睁眼标定 → 实时检测 → 导出 CSV → 画曲线」这个流程,不标定不上线。希望帮到你。

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

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

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

立即咨询