多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MediaPipe人脸关键点检测实战:环境搭建、实时检测与眨眼识别

MediaPipe人脸关键点检测实战:环境搭建、实时检测与眨眼识别 简介一套基于MediaPipe媒体管线框架的Python脸部识别源码包面向计算机视觉入门开发者与Python工程师可用于人脸关键点检测、手势跟踪、姿态估计等实时视觉任务也支持在此基础上二次开发个性化应用。压缩包共47个文件包括24个py脚本、11个h头文件、10个cc源文件以及2个build构建文件。py脚本覆盖人脸网格、手势识别、人体姿态等官方解决方案C源文件与头文件则展示底层计算图和数据管道实现目录划分清晰全部文件仅80KB离线阅读和改造非常方便。该资源已有1231人学习下载是理解MediaPipe内部工作原理的高性价比素材。仔细研读这些代码既能掌握面部468个关键点的检测流程也能借助calculator_graph、packet等基础模块理解实时推理的调度机制进而扩展出表情识别、身份验证等更丰富的功能为后续模型部署与性能优化打下基础。1. 用 Python 做脸部识别MediaPipe 是现在最值得先跑通的那条路如果你搜过“python 脸部识别”和“MEDIAPIPE”大概率是已经受够了 OpenCV 自带 Haar 级联的低识别率或者被 dlib 的模型文件和环境依赖折腾到没脾气。MediaPipe 是 Google 开源的跨平台多媒体机器学习框架它的 FaceMesh 模型能在纯 CPU 上实时输出 468 个人脸关键点不需要显卡不需要训练自己的模型Python 环境下装一个包就能用。这篇文章会直接带你从环境搭建走到摄像头实时检测再给出眨眼检测、张嘴判定这类能直接改造成考勤、疲劳提醒、注意力统计的小功能。新手照着敲就能跑通熟手可以重点看第四章的参数边界和第五章的避坑清单这些都是实际项目里最容易翻车的地方。2. 环境搭建用 pip 装对 MediaPipe 和 OpenCV先避开 Python 版本埋的雷2.1 Python 版本与依赖选型为什么很多人卡在安装这一步MediaPipe 的安装问题常年排在 Python 报错热榜前面绝大多数翻车现场不是代码问题而是 Python 版本和依赖冲突。MediaPipe 的 PyPI 包对 Python 版本有明确要求0.10.x 系列目前支持 Python 3.8 到 3.12但如果你用的是 3.7 或者更老的版本pip 会直接报找不到匹配版本。同理OpenCV 的包名是 opencv-python不是 cv2很多人pip install cv2报错就是因为包名搞错了。我一般建议新项目直接装 Python 3.10 或 3.11这两个版本对 MediaPipe、OpenCV、NumPy 的兼容性最稳。装完之后用pip list检查版本重点看 mediapipe、opencv-python、numpy 三个包的版本号。NumPy 的版本也容易出问题MediaPipe 某些版本对 NumPy 2.x 支持不好如果导入时报_ARRAY_API not found就把 NumPy 降到 1.26.4。2.2 最小安装命令与验证脚本在命令行里依次执行下面的安装命令。建议用虚拟环境别直接往系统 Python 里塞后面项目多了你就知道这步有多值得。# 创建虚拟环境Windows 和 Linux/macOS 通用 python -m venv face_env # 激活虚拟环境 # Windows: face_env\Scripts\activate # Linux/macOS: source face_env/bin/activate # 安装核心依赖 pip install mediapipe opencv-python numpy安装完成后用一小段代码验证环境是否正常。这一步花不了 30 秒但能过滤掉后续一大半的莫名其妙问题。import mediapipe as mp import cv2 # 打印版本号确认安装成功 print(MediaPipe version:, mp.__version__) print(OpenCV version:, cv2.__version__) # 用一张纯色图测试 FaceMesh 初始化 import numpy as np img np.zeros((480, 640, 3), dtypenp.uint8) mp_face_mesh mp.solutions.face_mesh with mp_face_mesh.FaceMesh(static_image_modeTrue, max_num_faces1) as face_mesh: results face_mesh.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) print(FaceMesh init OK, detections:, len(results.multi_face_landmarks) if results.multi_face_landmarks else 0)这段代码的逻辑是先导入 mediapipe 和 cv2然后创建一张 640x480 的纯色图像传给 FaceMesh 处理。如果环境正常你会看到两个版本号输出以及FaceMesh init OK。static_image_modeTrue表示对单张图片检测而非视频流max_num_faces1限制最多检测 1 张人脸。参数设置的逻辑是初始化模型只需要一次不要在循环里反复创建 FaceMesh 实例否则内存会持续上涨这是新手最容易犯的错。2.3 没有摄像头时的替代验证方案很多人在服务器或者虚拟机上跑没有摄像头这时候不要急着放弃。MediaPipe 支持传入图片文件和视频文件你可以先下载一张包含人脸的测试图片用cv2.imread读进来检测管线完全一致只是把输入源从摄像头替换成文件而已。import cv2 import mediapipe as mp mp_face_mesh mp.solutions.face_mesh mp_drawing mp.solutions.drawing_utils mp_drawing_styles mp.solutions.drawing_styles # 读取本地图片 image cv2.imread(test_face.jpg) rgb_image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) with mp_face_mesh.FaceMesh(static_image_modeTrue, max_num_faces5, refine_landmarksTrue) as face_mesh: results face_mesh.process(rgb_image) if results.multi_face_landmarks: print(f检测到 {len(results.multi_face_landmarks)} 张人脸) for face_landmarks in results.multi_face_landmarks: mp_drawing.draw_landmarks( imageimage, landmark_listface_landmarks, connectionsmp_face_mesh.FACEMESH_TESSELATION, landmark_drawing_specNone, connection_drawing_specmp_drawing_styles.get_default_face_mesh_tesselation_style()) else: print(未检测到人脸) cv2.imshow(FaceMesh, image) cv2.waitKey(0) cv2.destroyAllWindows()这段代码里refine_landmarksTrue是关键参数它打开虹膜关键点检测让眼部周边的 13 个额外关键点可用。对后续做眨眼检测、视线估计来说这个参数是必开的默认是 False。检测到人脸后用draw_landmarks把网格画到原图上FACEMESH_TESSELATION是脸部三角剖分的连接关系画出密网格看着更直观。3. 原理与模型选型FaceMesh 的 468 个关键点到底怎么用3.1 为什么是 MediaPipe 而不是 Haar 或 dlibOpenCV 自带的 Haar 级联人脸检测只能给出一个矩形框你要想判断眼睛是否闭上、嘴巴是否张开就得在框内再做一轮图像处理准确率很难看。dlib 的 68 点模型效果不错但是要下载约 100MB 的预训练模型文件安装配置也比较繁琐而且 CPU 推理速度大约只有 30-40 FPS 左右。MediaPipe FaceMesh 的优势是慢速设备也能跑实时。它用 blob 卷积网络结构做关键点回归一次性输出 468 个三维关键点并且把关键点分成多个子集。FACEMESH_LIPS 里的关键点编号 0 到 13 是嘴唇轮廓FACEMESH_LEFT_EYE 和 FACEMESH_RIGHT_EYE 对应左右眼轮廓FACEMESH_FACE_OVAL 对应脸型轮廓。这些子集连接关系可以直接从mp.solutions.face_mesh.FACEMESH_XXX常量里取不需要手动定义。3.2 关键点坐标系与坐标归一化最容易算错的一步FaceMesh 输出的landmark.x和landmark.y是归一化坐标范围在 0 到 1 之间表示相对图像宽高的比例。拿 640x480 的图像来说某关键点归一化坐标是 (0.5, 0.5)实际像素坐标就是 (320, 240)。这里实际项目里有大量人栽跟头——直接拿归一化坐标去算像素距离得到的结果是错的。正确的换算方式是h, w, _ image.shape x_pixel int(landmark.x * w) y_pixel int(landmark.y * h)而 z 坐标表示关键点在人脸深度方向的距离单位是相对人脸尺寸的比例。注意z 轴的原点在鼻尖附近往前为正方向这意味着它不是一个绝对深度值不同人脸之间的 z 值不宜直接比较但在同一张脸上算角度相对位移是足够的。3.3 搭建一个通用的人脸关键点检测类实际做项目的时候不要每次都写一段面向过程的 OpenCV 循环。把 FaceMesh 的初始化、图像处理、关键点坐标转换封装成一个类后面切摄像头、切视频、切图片都是同一套接口。import cv2 import mediapipe as mp import numpy as np class FaceMeshDetector: def __init__(self, static_modeFalse, max_faces2, refine_landmarksTrue, min_detection_confidence0.5): self.mp_face_mesh mp.solutions.face_mesh self.face_mesh self.mp_face_mesh.FaceMesh( static_image_modestatic_mode, max_num_facesmax_faces, refine_landmarksrefine_landmarks, min_detection_confidencemin_detection_confidence) self.mp_drawing mp.solutions.drawing_utils self.mp_drawing_styles mp.solutions.drawing_styles def get_landmarks(self, image): 输入 BGR 图像返回人脸关键点列表。 每个关键点已换算为像素坐标和归一化坐标两种形式。 rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results self.face_mesh.process(rgb) faces_data [] if results.multi_face_landmarks: h, w image.shape[:2] for face_landmarks in results.multi_face_landmarks: points_px [] points_norm [] for lm in face_landmarks.landmark: points_px.append((int(lm.x * w), int(lm.y * h))) points_norm.append((lm.x, lm.y, lm.z)) faces_data.append({ landmarks_px: points_px, landmarks_norm: points_norm, face_landmarks: face_landmarks }) return faces_data def draw_mesh(self, image, face_landmarks, connectionsNone): 在图像上绘制关键点网格。 if connections is None: connections self.mp_face_mesh.FACEMESH_TESSELATION self.mp_drawing.draw_landmarks( image, face_landmarks, connections, landmark_drawing_specNone, connection_drawing_specself.mp_drawing_styles .get_default_face_mesh_tesselation_style()) def close(self): self.face_mesh.close()min_detection_confidence0.5是检测置信度阈值默认值就是 0.5。把它调到 0.7 会减少误检但可能漏掉侧脸或遮挡脸调到 0.3 则相反。实际摄像头场景下0.5 是一个经过验证的平衡点。static_modeFalse表示视频流模式MediaPipe 内部会利用帧间时序信息做关键点跟踪能够显著提升检测速度代价是如果某帧突然检测不到人脸后面几帧可能连续丢失这个特性在第五章的坑位里还要提到。4. 实时摄像头人脸关键点检测从单帧到视频流的完整管线4.1 视频流主循环与性能参数设定在摄像头场景下MediaPipe FaceMesh 的处理逻辑和静态图片完全一致区别在于视频流需要循环读取摄像头帧并且把上一帧的人脸跟踪结果传给下一帧加速检测。下面这段代码是可以在本地直接跑通的实时检测程序。import cv2 import mediapipe as mp mp_face_mesh mp.solutions.face_mesh mp_drawing mp.solutions.drawing_utils mp_drawing_styles mp.solutions.drawing_styles # 初始化摄像头0 表示第一个摄像头设备 cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30) # 创建 FaceMesh 实例视频流模式 with mp_face_mesh.FaceMesh( static_image_modeFalse, max_num_faces2, refine_landmarksTrue, min_detection_confidence0.5, min_tracking_confidence0.5 ) as face_mesh: while cap.isOpened(): success, image cap.read() if not success: print(无法读取摄像头画面请检查设备) break # 水平翻转让画面和镜子里的自己一致避免左右手混淆 image cv2.flip(image, 1) # 提升性能检测前将图像标记为不可写MediaPipe 内部会优化内存拷贝 image.flags.writeable False rgb_image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results face_mesh.process(rgb_image) # 恢复图像可写准备绘制关键点 image.flags.writeable True if results.multi_face_landmarks: for face_landmarks in results.multi_face_landmarks: mp_drawing.draw_landmarks( imageimage, landmark_listface_landmarks, connectionsmp_face_mesh.FACEMESH_CONTOURS, landmark_drawing_specNone, connection_drawing_specmp_drawing_styles .get_default_face_mesh_contours_style()) # 显示画面按 ESC 退出 cv2.imshow(MediaPipe FaceMesh, image) key cv2.waitKey(1) 0xFF if key 27: break cap.release() cv2.destroyAllWindows()代码里有三个值得说明的地方。第一image.flags.writeable False是 MediaPipe 官方推荐的高频操作它告诉 OpenCV 这一步之后不再修改这块内存MediaPipe 可以把输入数据直接传给推理后端而避免一次拷贝在低端 CPU 上能明显提升帧率。第二cv2.flip(image, 1)做水平镜像否则你在画面里举手检测出的关键点位置和你的实际动作是相反的做交互项目时这是个容易被忽略的细节。第三FACEMESH_CONTOURS只绘制脸部轮廓、眼睛、嘴唇的关键点连线比 FACEMESH_TESSELATION 的全网格更干净视觉上不遮挡人脸。4.2 关键点索引表468 个点里最常用的几组MediaPipe FaceMesh 的 468 个关键点看起来让人头皮发麻但你真正用到的永远是几组固定编号官方文档里这些编号是公开的。以下是我在项目中最常用的索引分组可以直接复制到代码里当作常量。区域关键点索引部分典型用途左眼轮廓[33, 160, 158, 133, 153, 144]眨眼检测、眼睑开合度计算右眼轮廓[362, 385, 387, 263, 373, 380]同理注意左右眼索引不镜像嘴部外轮廓[61, 146, 91, 181, 84, 17]张嘴判定、说话检测嘴部内轮廓[0, 17, 14, 13, 12, 11]张嘴幅度计算、语音辅助鼻尖[1, 2, 4, 5, 6]头部姿态估计基准点脸部轮廓[10, 152, 234, 454]人脸对齐、脸型分析一个容易被问到的点左右眼的索引为什么不对称因为 FaceMesh 输出的关键点顺序是按人脸解剖结构定义的左眼是 33 到 133 这一组右眼是 362 到 263 这一组不要试图用索引对称去套直接用上面这组编号就行。4.3 用几何特征判断眼睛状态眨眼检测的最小实现拿到关键点坐标之后最常见的需求是判断眼睛是否闭合。这里用眼睛纵横比EAREye Aspect Ratio算法它只需要 6 个关键点的二维像素坐标计算两段垂直距离的平均值除以水平距离比值低于阈值就认为闭眼。这个算法在 dlib 时代是主流方案在 FaceMesh 下依然适用。def eye_aspect_ratio(eye_points): 计算眼睛纵横比。eye_points 是 6 个关键点的 (x, y) 列表。 # 垂直距离两组相对的点 vert_1 np.linalg.norm(np.array(eye_points[1]) - np.array(eye_points[5])) vert_2 np.linalg.norm(np.array(eye_points[2]) - np.array(eye_points[4])) # 水平距离一对最远的点 horiz np.linalg.norm(np.array(eye_points[0]) - np.array(eye_points[3])) return (vert_1 vert_2) / (2.0 * horiz)调用时把上一节索引表里的关键点像素坐标传进去以左眼为例LEFT_EYE_IDX [33, 160, 158, 133, 153, 144] RIGHT_EYE_IDX [362, 385, 387, 263, 373, 380] # 假设 faces_data 是检测类返回的某张人脸数据 left_eye_pts [faces_data[landmarks_px][i] for i in LEFT_EYE_IDX] right_eye_pts [faces_data[landmarks_px][i] for i in RIGHT_EYE_IDX] left_ear eye_aspect_ratio(left_eye_pts) right_ear eye_aspect_ratio(right_eye_pts) ear (left_ear right_ear) / 2.0 # 经验阈值正常睁眼时 EAR 约 0.25-0.35闭合时低于 0.15 if ear 0.2: print(眼睛处于闭合状态)EAR 的计算逻辑不复杂但有两个参数需要根据摄像头距离调阈值 0.2 是通用经验值离摄像头远时人脸占画面小关键点抖动放大阈值可以放宽到 0.25连续闭眼帧数要设定一个计数器连续 3 到 5 帧判定为一次眨眼避免单帧噪声误触发。视线距离不同导致的关键点像素距离变化是夸大误差的主要来源后面避坑章节会单独展开。5. 常见坑位排查MediaPipe 项目里最常翻车的 5 个问题5.1 摄像头打不开报错cant open camera by index现象cv2.VideoCapture(0)一直返回 False或者程序直接报错退出。原因三种情况最常见。第一摄像头被其他应用占用比如 Zoom、微信视频会议还在后台运行第二笔记本摄像头被物理开关或系统隐私设置禁用了第三在 Linux 系统上设备节点没有权限访问/dev/video0。解决先关掉所有占用摄像头的应用再在系统设置里检查摄像头权限。如果用的是虚拟机需要在虚拟机设置里把 USB 摄像头或主机摄像头直通给虚拟机。Windows 系统可以在设置 → 隐私 → 相机里确认允许桌面应用访问摄像头。Linux 下用ls /dev/video*查看设备列表然后sudo chmod 666 /dev/video0临时改权限或者把用户加入video组。5.2 检测速度很慢FPS 只有个位数现象摄像头画面卡顿CPU 占用率极高。注意这里指普通电脑 CPU 运行慢不是指设备本身性能弱。原因最常见的是static_image_modeTrue被留在了实时循环里这样每帧都做完整的人脸检测而不是用跟踪结果速度会降到十几毫秒甚至更低到几百毫秒。其次是没有调用image.flags.writeable False每帧多一次内存拷贝。第三个原因是在循环里反复创建 FaceMesh 实例。解决视频流模式必须使用static_image_modeFalse并且把 FaceMesh 初始化放在循环外。用cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)和cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)把分辨率限制在 640x480不要用 1080P 摄像头数据直接喂给 FaceMesh推理时间和分辨率几乎成正比。另一个有效手段是每隔一帧处理一次if frame_count % 2 0: process(frame)牺牲少量帧率换取超过一倍的性能提升。以上是实践中比较有效的做法具体效果取决于你的 CPU。5.3 人脸检测框闪烁关键点跳来跳去现象人脸静止不动但关键点在脸附近来回抖动画面看起来像“糊了一层果冻”。原因MediaPipe 的跟踪结果天然带有轻微抖动特别是min_tracking_confidence设置过低时跟踪和检测之间频繁切换造成关键点位置大幅跳变。另外低分辨率下关键点的像素精度天然降低。解决对关键点坐标做时间平滑最轻量的是指数移动平均。代码实现很简单smoothed_x alpha * current_x (1 - alpha) * previous_x smoothed_y alpha * current_y (1 - alpha) * previous_yalpha越大跟随越灵敏但抖动越大越小越平滑但延迟越高。建议从 0.3 开始调实测在普通笔记本摄像头上效果不错。更高级的做法是 Kalman 滤波或 One Euro Filter不过对大多数项目来说指数平滑已经够用了。5.4 人脸一转头就丢失再回正也不能立即恢复现象人脸左右转动超过约 45 度后FaceMesh 检测丢失回到正面后要等待数百毫秒甚至更久才重新检测到。原因视频流模式下 MediaPipe 启用跟踪逻辑检测不是每帧都做。当跟踪丢失后需要重新触发一次完整检测这个时间窗口里持续无输出。min_detection_confidence设置过高也会降低重检测的灵敏度。解决处理方式是当连续 N 帧没检测到人脸时临时把帧传入一个static_image_modeTrue的检测器做一次全图检测或者直接调低min_detection_confidence到 0.3 左右。min_tracking_confidence保持 0.5 以上让跟踪更稳定些。如果要保证检测一直在可以把min_detection_confidence和min_tracking_confidence都设为 0.7切换策略是当前帧人脸距离摄像头较远时降低置信度。5.5 多人场景只检测到一张脸max_num_faces无效现象画面里有两个人但只检测到一个人或者明明设置了max_num_faces2仍然只输出一张脸的结果。原因max_num_faces是上限值不是“保证检测两张脸”。MediaPipe 的检测器可能因为人脸距离太远、尺寸太小、遮挡或者光线造成其中一张没被检出。很多时候是你自己期望太高模型并不是真的有问题。解决把max_num_faces从 2 调到 4注意它会影响内存占用和推理时间CPU 上 4 张脸大约是 1 张脸的两倍耗时具体看设备。更重要的做法是让画面中的人脸保持足够大的占比如果人脸像素宽度小于 80 到 100 像素检测率会大幅下降。环境光不足时补光比调参更管用。6. 进阶用法用眨眼时长判断疲劳状态并把检测结果记录到本地一个很实用的进阶功能是把眨眼检测和时长统计结合起来。正常人眨眼持续时间在 100 到 400 毫秒之间如果你测得某段时间内每次眨眼都超过 500 毫秒并且每分钟眨眼次数显著低于正常值正常是每分钟 15 到 20 次大概率是视线正在长时间离开屏幕或注意力涣散。用 FaceMesh 做疲劳预警的核心代码只比前一章的嘴边检测多了一个计时器。import time # 全局状态 close_start_time None blink_total 0 blink_duration_threshold 0.5 # 单位秒超过这个值判定为“明显闭眼” closed_duration 0.0 # 在检测循环里 is_eye_closed ear 0.2 if is_eye_closed and close_start_time is None: close_start_time time.time() elif not is_eye_closed and close_start_time is not None: closed_duration time.time() - close_start_time close_start_time None if closed_duration blink_duration_threshold: blink_total 1 print(f检测到第 {blink_total} 次明显闭眼持续 {closed_duration:.2f} 秒)这块逻辑要放在视频循环内闭眼开始时记录时间戳睁眼时计算持续时间。如果持续时长超过阈值就记录一次“明显闭眼”。测试你会发现阈值 0.5 秒可以过滤掉绝大多数正常眨眼低于这个值的都算瞬目反射。如果你按这个流程完整走一遍实测在普通笔记本 CPU无需独立显卡上640x480 分辨率下帧率基本能维持在 20 到 30 FPS。想再压榨性能就把输入分辨率降到 480x360损失一点检测精度换流畅度在很多嵌入式设备上是值得的。还有一个小习惯FaceMesh实例用完后调用close()释放特别是长时间运行的程序不然反复初始化会让内存占用慢慢爬上去这个和 OpenCV 的release()一样重要。希望这篇一线趟坑笔记能帮你在 MediaPipe 脸部识别这条路上少走几步弯路。本文还有配套的精品资源点击获取
返回列表