资讯详情

基于MediaPipe的实时疲劳与坐姿检测系统:关键点几何计算

📅 2026/10/11 11:45:38 | 华诺云谱 👁 阅读
基于MediaPipe的实时疲劳与坐姿检测系统:关键点几何计算
简介一款基于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 或 YOLO2.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_faces1, # 只处理一个人省算力 refine_landmarksTrue, # 开启瞳孔关键点眼睑判定更稳 min_detection_confidence0.5, min_tracking_confidence0.5 ) pose mp_pose.Pose( min_detection_confidence0.5, min_tracking_confidence0.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(maxlenfps * 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_faces1 直接砍掉多人人脸网格的计算量refine_landmarksTrue 必须开不开就拿不到瞳孔位置眼睛六点法的精度会下降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_landmarksTrue。把 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_frames100): 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 → 画曲线」这个流程不标定不上线。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑