MediaPipe实战5分钟实现实时人脸关键点检测与自定义嘴唇换色附完整代码最近在捣鼓一些有趣的视觉应用发现MediaPipe真是个宝藏库。它把那些复杂的机器学习模型封装得极其友好让开发者能像搭积木一样快速构建应用。特别是人脸关键点检测以前觉得要写一堆OpenCV代码、调参、训练模型现在几行Python就能跑起来还能实时处理摄像头画面。这让我萌生了一个想法能不能在检测到人脸后做个好玩又实用的功能比如实时给嘴唇换个颜色这不仅是简单的美颜滤镜更涉及到关键点的精准定位、区域分割和图像融合。今天我就把这个从零搭建的过程连同完整的代码和踩过的坑分享给你。无论你是想做个趣味Demo还是为更复杂的AR特效打基础这篇文章都能给你一个清晰的起点。1. 环境准备与MediaPipe初探在开始写代码之前我们需要先把舞台搭好。这个项目主要依赖三个核心库MediaPipe、OpenCV和NumPy。MediaPipe负责核心的人脸网格检测算法OpenCV负责图像捕获、处理和显示NumPy则进行高效的数组运算。我强烈建议使用Anaconda或虚拟环境来管理依赖避免包版本冲突。首先通过pip安装必要的库pip install mediapipe opencv-python numpy安装过程通常很顺利。接下来我们快速了解一下MediaPipe的Face Mesh模型。它不是一个简单的“点检测”模型而是一个能输出468个3D人脸关键点的密集网格模型。这些点覆盖了脸部轮廓、嘴唇、眼睛、眉毛甚至虹膜的边缘。模型有两种运行模式static_image_mode和实时视频模式。对于摄像头应用我们肯定选择实时模式它能利用上一帧的检测结果来优化当前帧的追踪大幅提升效率。注意MediaPipe的模型文件会在首次运行时自动下载。请确保网络通畅下载路径通常位于用户主目录下的缓存文件夹中。为了让你对MediaPipe的能力有个直观感受我们先来看一个最简单的“Hello World”级别的代码它只负责初始化并打印模型信息import mediapipe as mp # 初始化MediaPipe人脸网格解决方案 mp_face_mesh mp.solutions.face_mesh print(f可用的连接组: {dir(mp_face_mesh)}) # 通常会看到FACEMESH_TESSELATION, FACEMESH_CONTOURS, FACEMESH_IRISES等运行这段代码不会有什么视觉输出但它确认了你的环境已正确配置并且让你知道MediaPipe已经为我们预定义好了人脸各个部位如轮廓、虹膜的关键点连接方式这为我们后续的绘制和区域选择提供了极大便利。2. 搭建实时人脸关键点检测系统有了基础认知我们现在动手搭建一个实时检测的骨架。这个系统的核心流程是打开摄像头 - 循环读取帧 - 将帧转换为RGB格式MediaPipe的输入要求- 送入Face Mesh模型处理 - 在原始BGR帧上绘制检测结果 - 显示画面。让我们直接看代码我会逐段解释import cv2 import mediapipe as mp import numpy as np # 初始化MediaPipe人脸网格 mp_face_mesh mp.solutions.face_mesh mp_drawing mp.solutions.drawing_utils mp_drawing_styles mp.solutions.drawing_styles # 创建Face Mesh实例配置参数 face_mesh mp_face_mesh.FaceMesh( static_image_modeFalse, # False表示用于视频流启用追踪 max_num_faces1, # 最多检测一张脸可根据需要调整 refine_landmarksTrue, # 启用更精细的眼部和嘴唇关键点 min_detection_confidence0.5, # 检测置信度阈值 min_tracking_confidence0.5 # 追踪置信度阈值 ) # 初始化摄像头 cap cv2.VideoCapture(0) # 参数0通常代表默认摄像头 if not cap.isOpened(): print(错误无法打开摄像头。) exit() print(按 q 键退出程序。) while cap.isOpened(): success, image cap.read() if not success: print(忽略空摄像头帧。) continue # 为了提升性能可以将图像标记为不可写以传递引用 image.flags.writeable False # 将BGR图像转换为RGB image_rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 处理图像并检测人脸网格 results face_mesh.process(image_rgb) # 将图像标记回可写状态以进行绘制 image.flags.writeable True image cv2.cvtColor(image_rgb, cv2.COLOR_RGB2BGR) # 如果检测到人脸 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_TESSELATION, landmark_drawing_specNone, connection_drawing_specmp_drawing_styles .get_default_face_mesh_tesselation_style() ) # 绘制面部轮廓下巴、眼睛、嘴唇外缘等 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() ) # 绘制虹膜轮廓需要refine_landmarksTrue mp_drawing.draw_landmarks( imageimage, landmark_listface_landmarks, connectionsmp_face_mesh.FACEMESH_IRISES, landmark_drawing_specNone, connection_drawing_specmp_drawing_styles .get_default_face_mesh_iris_connections_style() ) # 水平翻转图像以获得自拍视图的镜像效果 image cv2.flip(image, 1) # 显示结果图像 cv2.imshow(MediaPipe Face Mesh, image) if cv2.waitKey(5) 0xFF ord(q): break # 释放资源 cap.release() cv2.destroyAllWindows()运行这段代码你应该能看到一个实时窗口你的脸上被覆盖了一个精致的白色网格线。这里有几个关键点值得深入参数调优min_detection_confidence和min_tracking_confidence是两个重要的阈值。前者控制何时触发一次全新的“检测”后者控制何时在帧间维持“追踪”。在光线不佳或人脸快速移动时适当调低如0.3可能获得更连续的体验但可能会引入一些误检。你可以根据实际场景调整。性能考量image.flags.writeable False这行代码是一个小技巧。它告诉OpenCV这块图像内存是只读的MediaPipe内部可能会直接使用这个内存引用而不复制数据对于高分辨率或高帧率视频这能带来轻微的性能提升。绘制风格mp_drawing_styles提供了预定义的绘制样式比如网格线的颜色和粗细。你也可以完全自定义创建自己的DrawingSpec对象。现在我们有了一个稳定运行的基础检测器。但你可能已经注意到我们只是绘制了连线还没有直接访问那468个关键点的具体坐标。别急下一步我们就来“抓住”这些点特别是嘴唇周围的点。3. 解析关键点数据与定位嘴唇区域MediaPipe检测到的face_landmarks是一个包含468个地标Landmark的对象列表。每个地标都有x,y,z三个属性分别代表归一化的坐标范围0到1和相对深度。我们的目标是从中筛选出构成上下嘴唇轮廓的点索引。首先我们需要将归一化坐标转换为图像上的实际像素坐标。公式很简单pixel_x landmark.x * image_width,pixel_y landmark.y * image_height。让我们修改一下循环内的绘制部分不仅绘制网格还把关键点坐标提取出来if results.multi_face_landmarks: for face_landmarks in results.multi_face_landmarks: # 获取图像尺寸 image_height, image_width, _ image.shape # 创建一个列表来存储所有关键点的像素坐标 landmark_points [] for idx, landmark in enumerate(face_landmarks.landmark): # 将归一化坐标转换为像素坐标 x int(landmark.x * image_width) y int(landmark.y * image_height) landmark_points.append([x, y]) # 可选在图像上绘制每个点用于调试 # cv2.circle(image, (x, y), 1, (0, 255, 0), -1) # cv2.putText(image, str(idx), (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (255, 0, 0), 1) # 将列表转换为NumPy数组方便后续处理 landmark_array np.array(landmark_points, dtypenp.int32) # 现在landmark_array就是一个形状为(468, 2)的数组包含了所有点的[x, y]坐标 # 我们可以根据预定义的索引来获取嘴唇区域那么嘴唇的索引是哪些呢MediaPipe有详细的文档说明每个索引对应的面部位置。对于嘴唇我们主要关注上下嘴唇的外缘。经过查阅和实践以下两组索引是常用的上嘴唇外缘索引[61, 185, 40, 39, 37, 0, 267, 269, 270, 409, 291, 308, 415, 310, 311, 312, 13, 82, 80, 191, 78, 61]下嘴唇外缘索引[78, 95, 88, 178, 87, 14, 317, 402, 318, 324, 308, 291, 375, 321, 405, 314, 17, 84, 181, 91, 146, 61, 78]注意列表的首尾通常是同一个点这是为了形成一个闭合的多边形。我们可以用这些索引从landmark_array中提取出嘴唇的坐标点集。为了更直观地理解这些点在脸上的分布我们可以创建一个简单的参考表格索引范围对应的面部区域包含关键点数量主要用途0 - 16下巴轮廓17定义脸部下半部分轮廓61, 78, 等嘴唇外缘约40个点嘴唇区域分割、口红/换色13, 14, 等嘴唇内部关键点多个点检测嘴唇开合、表情33 - 46右眉毛14表情分析46 - 59左眉毛14表情分析68 - 81鼻子轮廓与鼻尖14姿态估计、滤镜锚点133 - 150右眼轮廓18眼动追踪、美瞳特效151 - 168左眼轮廓18眼动追踪、美瞳特效468个点中的其余部分面部网格细化大量点3D面部重建、高精度特效有了坐标点集我们就可以用OpenCV的cv2.fillPoly函数来创建一个嘴唇区域的掩膜Mask。这个掩膜是一个二值图像在嘴唇区域为白色255其他区域为黑色0。这是我们实现换色的基础。4. 实现动态嘴唇换色与效果优化核心思路是利用嘴唇关键点生成掩膜 - 创建一个纯色图层 - 将纯色图层与掩膜结合得到嘴唇色块 - 将色块与原图进行透明叠加。为了让效果更自然我们还需要进行高斯模糊来柔化边缘以及调整叠加的透明度。我们将这个功能封装成一个函数并加入实时调整颜色的交互。这里我们用OpenCV的轨迹条Trackbar来动态调整BGR颜色值。首先定义换色函数def apply_lip_color(image, landmark_points, upper_lip_indices, lower_lip_indices, color_bgr, blur_kernel_size7, alpha0.4): 在图像上应用嘴唇颜色。 参数: image: 输入图像 (BGR格式)。 landmark_points: 所有468个关键点的像素坐标列表或数组形状为(N, 2)。 upper_lip_indices: 上嘴唇关键点索引列表。 lower_lip_indices: 下嘴唇关键点索引列表。 color_bgr: 要应用的颜色元组格式 (B, G, R)。 blur_kernel_size: 高斯模糊核大小必须是正奇数。越大边缘越柔和。 alpha: 颜色图层的透明度 (0.0 - 1.0)1.0为完全不透明。 # 1. 创建与原图同尺寸的空白掩膜 mask np.zeros(image.shape[:2], dtypenp.uint8) # 2. 提取上下嘴唇的坐标点并确保形状为(-1, 1, 2) upper_lip_pts landmark_points[upper_lip_indices].reshape((-1, 1, 2)) lower_lip_pts landmark_points[lower_lip_indices].reshape((-1, 1, 2)) # 3. 在掩膜上填充嘴唇区域为白色 cv2.fillPoly(mask, [upper_lip_pts], 255) cv2.fillPoly(mask, [lower_lip_pts], 255) # 4. 对掩膜进行高斯模糊使边缘过渡自然 mask_blurred cv2.GaussianBlur(mask, (blur_kernel_size, blur_kernel_size), 0) # 将掩膜归一化到0-1范围用于alpha混合 mask_normalized mask_blurred[:, :, np.newaxis].astype(np.float32) / 255.0 # 5. 创建纯色图像 color_layer np.zeros_like(image, dtypenp.float32) color_layer[:] color_bgr # 6. Alpha混合result image * (1 - alpha*mask) color_layer * (alpha*mask) # 这里我们让alpha参数控制整体颜色强度mask控制区域 blended image.astype(np.float32) * (1 - alpha * mask_normalized) color_layer * (alpha * mask_normalized) result_image blended.astype(np.uint8) return result_image接下来修改主循环集成轨迹条和换色功能import cv2 import mediapipe as mp import numpy as np # ... (Face Mesh初始化代码与之前相同) ... # 定义嘴唇索引 UPPER_LIP_INDICES [61, 185, 40, 39, 37, 0, 267, 269, 270, 409, 291, 308, 415, 310, 311, 312, 13, 82, 80, 191, 78, 61] LOWER_LIP_INDICES [78, 95, 88, 178, 87, 14, 317, 402, 318, 324, 308, 291, 375, 321, 405, 314, 17, 84, 181, 91, 146, 61, 78] # 创建一个窗口并添加BGR颜色轨迹条 cv2.namedWindow(Lip Color Filter) cv2.resizeWindow(Lip Color Filter, 640, 480) # 调整窗口大小 # 定义轨迹条回调函数什么也不做 def nothing(x): pass # 创建三个轨迹条分别对应B, G, R通道初始值设为红色 (0, 0, 255) cv2.createTrackbar(B, Lip Color Filter, 0, 255, nothing) cv2.createTrackbar(G, Lip Color Filter, 0, 255, nothing) cv2.createTrackbar(R, Lip Color Filter, 255, 255, nothing) # 可以再添加一个控制透明度的轨迹条 cv2.createTrackbar(Alpha, Lip Color Filter, 40, 100, nothing) # 0-100对应0.0-1.0 cap cv2.VideoCapture(0) while cap.isOpened(): success, frame cap.read() if not success: break # 水平翻转获得镜像效果 frame cv2.flip(frame, 1) image_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) image_rgb.flags.writeable False results face_mesh.process(image_rgb) image_rgb.flags.writeable True working_image cv2.cvtColor(image_rgb, cv2.COLOR_RGB2BGR) if results.multi_face_landmarks: # 假设只处理检测到的第一张脸 face_landmarks results.multi_face_landmarks[0] h, w, _ working_image.shape landmark_points [] for landmark in face_landmarks.landmark: x, y int(landmark.x * w), int(landmark.y * h) landmark_points.append([x, y]) landmark_array np.array(landmark_points, dtypenp.int32) # 从轨迹条获取当前颜色和透明度 b cv2.getTrackbarPos(B, Lip Color Filter) g cv2.getTrackbarPos(G, Lip Color Filter) r cv2.getTrackbarPos(R, Lip Color Filter) alpha_slider cv2.getTrackbarPos(Alpha, Lip Color Filter) alpha alpha_slider / 100.0 # 转换为0.0-1.0 target_color (b, g, r) # 应用换色函数 working_image apply_lip_color(working_image, landmark_array, UPPER_LIP_INDICES, LOWER_LIP_INDICES, target_color, alphaalpha) # 显示结果 cv2.imshow(Lip Color Filter, working_image) if cv2.waitKey(5) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()现在运行程序你会看到一个带有三个轨迹条的窗口。拖动滑块可以实时改变嘴唇的颜色和透明度。你会发现直接叠加纯色可能看起来有点“假”特别是颜色很鲜艳的时候。这就是我们之前加入高斯模糊和透明度混合的原因它们能模拟出唇彩的质感。提示如果换色区域边缘有锯齿或不够贴合可以尝试调整apply_lip_color函数中的blur_kernel_size参数例如从7增加到11或者检查嘴唇索引列表是否准确覆盖了你的嘴唇轮廓。不同的人脸形状可能需要微调索引点。5. 高级技巧性能优化与扩展思路一个基础的Demo跑起来后我们通常会考虑如何让它更流畅、更健壮或者功能更强大。这里分享几个我在项目迭代中总结的点。性能优化 实时视频处理对性能敏感。如果感觉帧率较低可以尝试以下方法降低处理分辨率不需要用摄像头最高分辨率进行处理。# 在循环开始前设置或对读取的帧进行缩放 def resize_frame(frame, scale_percent50): width int(frame.shape[1] * scale_percent / 100) height int(frame.shape[0] * scale_percent / 100) return cv2.resize(frame, (width, height), interpolationcv2.INTER_AREA) # 在process之前对image_rgb使用此函数跳帧处理不是每一帧都进行检测。可以每2帧或3帧处理一次中间帧沿用上一帧的关键点位置。这对于头部移动不快的场景很有效。限制检测区域如果人脸大致在画面中央可以只对图像的一个子区域进行检测减少计算量。扩展思路 这个嘴唇换色的Demo可以作为一个跳板扩展到更多有趣的应用虚拟试妆不止是纯色可以加载带有纹理的唇彩图片将其变形Warp适配到嘴唇形状上实现更真实的试妆效果。这需要用到OpenCV的透视变换或薄板样条插值。表情驱动动画通过分析嘴唇关键点例如上唇中点与下唇中点的距离来判断嘴巴是张开还是闭合进而控制一个虚拟形象的嘴部动画。美颜与修饰结合皮肤区域检测可以对牙齿进行美白或者轻微调整嘴唇的形状通过移动关键点再重新渲染。多脸支持与颜色随机修改max_num_faces参数并在循环中遍历results.multi_face_landmarks可以为画面中的多张脸应用换色甚至可以给每张脸随机分配一个颜色。一个常见的问题是在侧脸或部分遮挡时检测可能不稳定导致关键点抖动进而使换色区域闪烁。一个简单的缓解方案是加入关键点平滑滤波比如使用一个移动平均滤波器或卡尔曼滤波器来平滑连续帧之间的关键点位置。# 简易移动平均滤波示例 history_length 5 point_history [] # 应是一个长度为history_length的坐标列表队列 def smooth_landmarks(current_points, history): history.append(current_points.copy()) if len(history) history_length: history.pop(0) # 对历史坐标求平均 smoothed_points np.mean(history, axis0).astype(np.int32) return smoothed_points把这个平滑函数应用到从landmark_array中提取的嘴唇坐标上可以显著减少抖动让换色效果更稳定。当然滤波会引入一定的延迟需要根据应用场景在平滑度和实时性之间权衡。