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,方便你理解各模块之间的关系。
13.6 避坑指南
最后,分享几个我踩过的坑。
坑一:关键点抖动
实时检测时,关键点会轻微抖动。直接拿原始坐标做表情判断,结果会忽闪忽闪的。解决方案是做平滑滤波——我用的是指数移动平均(EMA),效果很好。
坑二:多人脸场景
MediaPipe支持最多检测10张脸,但每多一张脸,计算量翻倍。我在视频会议场景里,一般限制最多检测3张脸。超过3张,只追踪最近一次出现的人脸。
坑三:光线不足
暗光环境下,人脸检测的置信度会下降。我建议在摄像头采集端做自动增益控制(AGC),或者在前处理里做直方图均衡化。别指望模型自己扛,前端先处理一下,效果立竿见影。
好了,关于人脸检测与追踪,今天就聊这么多。核心就三件事:选对工具(MediaPipe或TF.js)、做好关键点映射、用对推理方式。下一节咱们聊聊更进阶的话题——但那是后话了。
公众号:蓝海资料掘金营,微信deep3321