13、人脸检测与追踪:MediaPipe FaceMesh、TensorFlow.js Face Detection、关键点映射、表情识别

人脸检测与追踪,是WebRTC与AI融合里最“接地气”的一环。为什么这么说?因为你在视频通话里看到的那些虚拟背景、美颜滤镜、甚至表情包自动生成,底层都离不开它。

我个人习惯把这块技术拆成两个层面:检测追踪。检测是“找到脸在哪”,追踪是“脸动了,我还跟得上”。今天咱们就聊聊MediaPipe FaceMesh和TensorFlow.js Face Detection这两套方案,再讲讲关键点映射和表情识别怎么落地。

13.1 MediaPipe FaceMesh:轻量级人脸网格

MediaPipe是Google开源的跨平台框架。FaceMesh是它里面专门做人脸关键点检测的模型。它能输出468个3D关键点,覆盖眉毛、眼睛、鼻子、嘴巴、下巴轮廓。

我在项目中遇到过一个问题:用MediaPipe跑实时视频流,CPU占用率一直降不下来。后来发现是没开GPU加速。MediaPipe默认支持OpenGL ES,你只要在初始化时指定DELEGATE_GPU,帧率能翻一倍。

核心优势:468个点,精度高,且自带3D坐标。这意味着你可以做头部姿态估计——比如判断用户是低头还是转头。

用法很简单。以JavaScript为例:

const faceMesh = new FaceMesh({
  locateFile: (file) => {
    return `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`;
  }
});

faceMesh.setOptions({
  maxNumFaces: 2,
  refineLandmarks: true,
  minDetectionConfidence: 0.5,
  minTrackingConfidence: 0.5
});

faceMesh.onResults((results) => {
  if (results.multiFaceLandmarks) {
    // 拿到468个关键点
    const landmarks = results.multiFaceLandmarks[0];
    // 做你想做的事
  }
});

注意那个refineLandmarks参数。我建议你打开它。它会让眼睛和嘴唇周围的点更密集,对表情识别特别有用。

13.2 TensorFlow.js Face Detection:浏览器端的另一种选择

TensorFlow.js也提供了人脸检测模型。它走的是“检测框+关键点”的路线。相比MediaPipe,它的模型更轻量,但关键点数量少一些——通常是6个或68个。

你想想看,如果你的应用只需要知道人脸位置和眼睛、嘴巴的大致坐标,TF.js就够用了。它不需要额外安装,直接在浏览器里跑。

const model = await faceLandmarksDetection.load(
  faceLandmarksDetection.SupportedPackages.mediapipeFacemesh
);

const predictions = await model.estimateFaces({
  input: videoElement,
  returnTensors: false,
  flipHorizontal: false
});

if (predictions.length > 0) {
  const keypoints = predictions[0].scaledMesh;
  // keypoints 就是468个点
}

嗯,这里要注意:TF.js的Face Detection底层其实也调用了MediaPipe的模型。所以如果你追求极致性能,直接用MediaPipe更直接。但如果你已经在用TensorFlow.js做其他AI任务,那统一用TF.js的API会更方便。

我的建议:如果项目是纯前端,且对帧率要求高(比如30fps以上),优先选MediaPipe。如果项目需要和后端模型联动,或者你已经在用TF.js做手势识别、物体检测,那就用TF.js的Face Detection。

13.3 关键点映射:从468个点到业务逻辑

拿到468个点之后,怎么用?说白了就是做映射。把点坐标映射到具体的业务含义上。

举个例子。我想判断用户是否在微笑。我不需要468个点,我只需要嘴巴周围的点。MediaPipe的468个点有固定的索引编号。嘴巴区域是索引0到17(上唇)和61到76(下唇)。

我常用的映射方式:

  • 眼睛闭合检测:取左眼上下眼睑的点(索引159、145),计算欧氏距离。距离小于阈值,说明闭眼了。
  • 嘴巴张开检测:取上唇和下唇中心点(索引13、14),计算垂直距离。
  • 头部偏转:取鼻尖(索引1)和左右耳根(索引234、454),计算三角关系。

我曾经踩过一个坑:直接用原始坐标做距离计算,结果用户一转头,距离就变了。后来我改用归一化坐标——把关键点相对于人脸框做归一化,这样不管人脸在画面哪个位置,距离值都稳定。

function getEyeAspectRatio(landmarks, eyeIndices) {
  // 取上下眼睑的点
  const p1 = landmarks[eyeIndices[0]];
  const p2 = landmarks[eyeIndices[1]];
  const p3 = landmarks[eyeIndices[2]];
  const p4 = landmarks[eyeIndices[3]];
  const p5 = landmarks[eyeIndices[4]];
  const p6 = landmarks[eyeIndices[5]];

  // 计算垂直距离
  const v1 = distance(p2, p6);
  const v2 = distance(p3, p5);
  // 水平距离
  const h = distance(p1, p4);

  return (v1 + v2) / (2.0 * h);
}

这个getEyeAspectRatio值如果低于0.2,基本可以判定闭眼了。我在做疲劳驾驶检测时就用这个逻辑,效果还不错。

13.4 表情识别:从关键点到情绪标签

表情识别,说白了就是给一组关键点打标签:高兴、悲伤、愤怒、惊讶、厌恶、恐惧、中性。

怎么做?有两种主流思路。

思路一:基于几何规则

直接利用关键点之间的几何关系。比如嘴巴宽度和高度比、眉毛上扬程度、眼睛张开程度。把这些特征组合起来,用简单的决策树或阈值判断。

优点是快,几乎不消耗算力。缺点是准确率有限,对复杂表情识别不好。

思路二:基于深度学习

把468个关键点作为输入,喂给一个轻量级分类模型。模型输出7个类别的概率。

我推荐用第二种。虽然需要一点模型训练,但准确率能到90%以上。而且模型可以很小——一个三层全连接网络就够了。

实战经验:我在做一个在线教育项目时,需要判断学生是否在认真听课。我们用MediaPipe提取关键点,然后用一个简单的MLP模型做表情分类。模型只有50KB,在浏览器里跑一次推理不到5ms。效果比纯规则好很多。

训练数据可以用FER2013或者AffectNet。但要注意,这些数据集是正面脸的。如果你的应用场景里用户经常侧脸,最好自己采集一些侧脸数据做微调。

13.5 整体架构图

下面这张图展示了从视频帧输入到表情识别的完整流程。我把它画成了SVG,方便你理解各模块之间的关系。

人脸检测与追踪:整体架构流程 视频帧输入 人脸检测(MediaPipe / TF.js) 468个3D关键点提取 几何规则(眼睛、嘴巴距离) 深度学习分类模型 表情标签 / 动作状态 输入层 检测层 特征层 推理层 输出层

13.6 避坑指南

最后,分享几个我踩过的坑。

坑一:关键点抖动

实时检测时,关键点会轻微抖动。直接拿原始坐标做表情判断,结果会忽闪忽闪的。解决方案是做平滑滤波——我用的是指数移动平均(EMA),效果很好。

坑二:多人脸场景

MediaPipe支持最多检测10张脸,但每多一张脸,计算量翻倍。我在视频会议场景里,一般限制最多检测3张脸。超过3张,只追踪最近一次出现的人脸。

坑三:光线不足

暗光环境下,人脸检测的置信度会下降。我建议在摄像头采集端做自动增益控制(AGC),或者在前处理里做直方图均衡化。别指望模型自己扛,前端先处理一下,效果立竿见影。

好了,关于人脸检测与追踪,今天就聊这么多。核心就三件事:选对工具(MediaPipe或TF.js)、做好关键点映射、用对推理方式。下一节咱们聊聊更进阶的话题——但那是后话了。


公众号:蓝海资料掘金营,微信deep3321