1.摘要
MediaPipe Face Mesh 是一种面部几何解决方案,即使在移动设备上也能实时估计 468 个 3D 面部标志。它采用机器学习 (ML) 来推断 3D 表面几何形状,只需要一个摄像头输入,无需专用深度传感器。该解决方案在整个管道中利用轻量级模型架构和 GPU 加速,提供对实时体验至关重要的实时性能。
此外,该解决方案与人脸几何模块捆绑在一起,弥合了人脸地标估计和有用的实时增强现实 (AR) 应用程序之间的差距。它建立了一个可度量的3D空间,并使用面部地标屏幕位置来估计该空间内的面部几何形状。人脸几何数据由常见的三维几何基元组成,包括人脸姿态变换矩阵和三角化人脸网格。在幕后,使用一种被称为普鲁克分析的轻量级的统计分析方法,用来驱动一个健壮的、性能好的、可移植的逻辑。该分析在CPU上运行,并且在ML模型推理的基础上具有最小的速度/内存占用。
2.机器学习流水线
我们的 ML 管道由两个协同工作的实时深度神经网络模型组成:一个对完整图像进行操作并计算人脸位置的检测器,以及一个对这些位置进行操作并通过回归预测近似表面几何形状的 3D 人脸地标模型。准确裁剪人脸大大减少了对常见数据增强的需求,例如由旋转、平移和缩放组成的仿射变换。相反,它允许网络将其大部分资源用于坐标预测精度。此外,在我们的管道中,还可以根据前一帧中识别出的人脸地标生成裁剪图,并且只有当地标模型无法再识别人脸时,才会调用人脸检测器来重新定位人脸。此策略类似于我们的 MediaPipe Hands 解决方案中采用的策略,后者使用手掌检测器和手部标志模型。
3.模型
- 人脸检测模型
该人脸检测器与MediaPipe人脸检测中使用的BlazeFace模型相同。 - 人脸地标模型
对于 3D 人脸地标,我们采用迁移学习训练了一个具有多个目标的网络:网络同时预测合成渲染数据上的 3D 地标坐标和带注释的真实世界数据上的 2D 语义轮廓。由此产生的网络为我们提供了合理的 3D 地标预测,不仅针对合成数据,还针对真实世界数据。
3D地标网络接收剪裁好的视频帧作为输入,不需要额外的深度输入。该模型输出3D点的位置,以及人脸在输入中存在和合理对齐的概率。一种常见的替代方法是预测每个地标的2D热图,但它不适合深度预测,而且对这么多点有很高的计算成本。我们通过迭代引导和改进预测进一步提高模型的准确性和鲁棒性。这样我们就可以将数据集扩展到越来越具有挑战性的情况,例如鬼脸、斜角和遮挡。
4.人脸几何模块
Face Landmark Model在屏幕坐标空间中进行单摄像头人脸地标检测:X、Y坐标为归一化屏幕坐标,而Z坐标为相对坐标,在弱透视投影摄像机模型下缩放为X坐标。这种格式非常适合一些应用程序,但它不能直接实现增强现实(AR)的全部功能,如将虚拟3D对象与检测到的人脸对齐。
人脸几何模块从屏幕坐标空间移向可度量的3D空间,并提供必要的基元来将检测到的人脸作为常规3D对象处理。通过设计,您将能够使用透视相机将最终的3D场景投影回屏幕坐标空间,并保证面部地标位置没有改变。
关键概念
4.1METRIC 3D SPACE
Face Geometry 模块中建立的 Metric 3D 空间是右手正交的 metric 3D 坐标空间。在空间内,有一个位于空间原点并指向Z轴负方向的虚拟透视相机。在当前的流程中,假设输入的相机帧正是由这个虚拟相机观察到的,因此它的参数稍后用于将屏幕地标坐标转换回Metric 3D空间。虚拟相机参数可以自由设置,但为了获得更好的效果,建议尽可能接近真实的物理相机参数。
4.2规范化的人脸模型
规范化的人脸模型(Canonical Face Model )是人脸的静态 3D 模型,它遵循 Face Landmark Model 的 468 个 3D 人脸地标拓扑。该模型具有两个重要功能:
- 定义了公制单位:规范化的人脸模型的比例定义了Metric 3D 空间的公制单位。默认规范化的人脸模型使用的公制单位是厘米;
- 静态空间和运行时空间之间的桥梁:人脸姿态变换矩阵实际上是从规范化的人脸模型到在每一帧上估计的运行时人脸地标集的线性映射。通过这种方式,围绕规范化的人脸模型建模的虚拟 3D 资产可以通过对它们应用人脸姿势变换矩阵来与跟踪的人脸对齐。
5.组件
5.1几何管道
几何管道是一个关键组件,它负责在Metric 3D空间内估计人脸几何对象。在每一帧中,按照给定的顺序执行以下步骤:
- 人脸地标屏幕坐标转换为三维空间坐标;
- 人脸姿态变换矩阵被估计为从规范化的人脸landmark 集到运行时人脸landmark 集的刚性线性映射,以最小化两者之间的差异;
- 使用运行时的人脸landmark作为顶点位置(XYZ)创建了一个人脸网格,而顶点纹理坐标(UV)和三角形拓扑都继承自规范化的人脸模型。
几何管道被实现为MediaPipe计算器。为了方便起见,人脸几何管道计算器与相应的元数据捆绑在一起,形成统一的MediaPipe子图。人脸几何格式被定义为协议缓冲区消息。
5.2效果渲染器
效果渲染器是一个组件,它作为一个面部效果渲染器的工作示例。它以OpenGL ES 2.0 API为目标,在移动设备上启用实时性能,并支持以下渲染模式:
- 3D物体渲染模式:虚拟对象与检测到的人脸对齐,以模拟附着在人脸上的对象(例如:眼镜);
- 人脸网格渲染模式:在面部网格表面上拉伸纹理以模拟面部绘画技术。
在这两种渲染模式中,面部网格首先被渲染为直接进入深度缓冲区的遮挡物。此步骤有助于通过隐藏面部表面后面的不可见元素来创建更可信的效果。
6.解决方案的API
6.1参数配置
-
STATIC_IMAGE_MODE
:如果设置为 false,该解决方案会将输入图像视为视频流。它将尝试在第一张输入图像中检测人脸,并在成功检测后进一步定位人脸地标。在随后的图像中,一旦检测到所有 max_num_faces 人脸并定位了相应的人脸地标,它就会简单地跟踪这些地标,而不会调用另一个检测,直到它失去对任何人脸的跟踪。这减少了延迟,非常适合处理视频帧。如果设置为 true,人脸检测会在每个输入图像上运行,非常适合处理一批静态的、可能不相关的图像。默认为false。 -
MAX_NUM_FACES
:要检测的最大人脸数。默认为 1。 -
MIN_DETECTION_CONFIDENCE
:来自人脸检测模型的最小置信值 ([0.0, 1.0]),以便将检测视为成功。默认为 0.5。 -
MIN_TRACKING_CONFIDENCE
:来自地标跟踪模型的最小置信值 ([0.0, 1.0]),用于将被视为成功跟踪的人脸地标,否则将在下一个输入图像上自动调用人脸检测。将其设置为更高的值可以提高解决方案的稳健性,但代价是更高的延迟。如果 static_image_mode 为真,则忽略这个参数,人脸检测将在每个图像上运行。默认为 0.5。
6.2输出
-
MULTI_FACE_LANDMARKS
:检测/跟踪人脸的集合,其中每个人脸表示为 468 个人脸地标的列表,每个地标由 x、y 和 z 组成。 x 和 y 分别通过图像宽度和高度归一化为 [0.0, 1.0]。 z 表示地标深度,以头部中心的深度为原点,值越小,地标离相机越近。 z 的大小使用与 x 大致相同的比例。
7.Python API解决方案
支持配置选项:
- static_image_mode
- max_num_faces
- min_detection_confidence
- min_tracking_confidence
(1)基础版本
# 图像
import cv2
import mediapipe as mp
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_utils.DrawingSpec
mp_face_mesh = mp.solutions.face_mesh
# 静态图片:
IMAGE_FILES = ["trump.jpg"]
drawing_spec = mp_drawing.DrawingSpec(thickness=1, circle_radius=1)
with mp_face_mesh.FaceMesh(
static_image_mode=True,
max_num_faces=1,
min_detection_confidence=0.5) as face_mesh:
for idx, file in enumerate(IMAGE_FILES):
image = cv2.imread(file)
# Convert the BGR image to RGB before processing.
results = face_mesh.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
# Print and draw face mesh landmarks on the image.
if not results.multi_face_landmarks:
continue
annotated_image = image.copy()
for face_landmarks in results.multi_face_landmarks:
print('face_landmarks:', face_landmarks)
mp_drawing.draw_landmarks(
image=annotated_image,
landmark_list=face_landmarks,
connections=mp_face_mesh.FACE_CONNECTIONS,)
mp_drawing.draw_landmarks(
image=annotated_image,
landmark_list=face_landmarks,
connections=mp_face_mesh.FACE_CONNECTIONS,)
cv2.imwrite('annotated_image' + str(idx) + '.png', annotated_image)
# 视频
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
import cv2
import mediapipe as mp
import time
cap = cv2.VideoCapture("1.mp4")
pTime = 0
mpDraw = mp.solutions.drawing_utils
mpFaceMesh = mp.solutions.face_mesh
faceMesh = mpFaceMesh.FaceMesh(max_num_faces=2)
drawSpec = mpDraw.DrawingSpec(thickness=1, circle_radius=2)
while True:
success, img = cap.read()
if not success:
break
imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
results = faceMesh.process(imgRGB)
if results.multi_face_landmarks:
for faceLms in results.multi_face_landmarks:
mpDraw.draw_landmarks(img, faceLms, mpFaceMesh.FACE_CONNECTIONS, drawSpec, drawSpec)
# mpDraw.draw_landmarks(img, faceLms, mpFaceMesh.FACE_CONNECTIONS)
for id, lm in enumerate(faceLms.landmark):
print(lm)
ih, iw, ic = img.shape
x, y = int(lm.x * iw), int(lm.y * ih)
print(id, x, y)
cTime = time.time()
fps = 1 / (cTime - pTime)
pTime = cTime
cv2.putText(img, f'FPS: {int(fps)}', (20, 70), cv2.FONT_HERSHEY_PLAIN,
3, (255, 0, 0), 3)
cv2.imshow("Image", img)
cv2.waitKey(1)
(2)加强版
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
"""
@File : FaceMeshModule.py.py
@Time : 2021/9/28 10:33
@Author : David
@Software: PyCharm
"""
import cv2
import mediapipe as mp
import time
class FaceMeshDetector():
def __init__(self, staticMode=False, maxFaces=2, minDetectionCon=0.5, minTrackCon=0.5):
self.staticMode = staticMode
self.maxFaces = maxFaces
self.minDetectionCon = minDetectionCon
self.minTrackCon = minTrackCon
self.mpDraw = mp.solutions.drawing_utils
self.mpFaceMesh = mp.solutions.face_mesh
self.faceMesh = self.mpFaceMesh.FaceMesh(self.staticMode, self.maxFaces,
self.minDetectionCon, self.minTrackCon)
self.drawSpec = self.mpDraw.DrawingSpec(thickness=1, circle_radius=2)
def findFaceMesh(self, img, draw=True):
self.imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
self.results = self.faceMesh.process(self.imgRGB)
faces = []
if self.results.multi_face_landmarks:
for faceLms in self.results.multi_face_landmarks:
if draw:
self.mpDraw.draw_landmarks(img, faceLms, self.mpFaceMesh.FACEMESH_CONTOURS,
self.drawSpec, self.drawSpec)
face = []
for id, lm in enumerate(faceLms.landmark):
# print(lm)
ih, iw, ic = img.shape
x, y = int(lm.x * iw), int(lm.y * ih)
# cv2.putText(img, str(id), (x, y), cv2.FONT_HERSHEY_PLAIN,
# 0.7, (0, 255, 0), 1)
# print(id,x,y)
face.append([x, y])
faces.append(face)
return img, faces
def main():
cap = cv2.VideoCapture("1.mp4")
pTime = 0
detector = FaceMeshDetector(maxFaces=2)
while True:
success, img = cap.read()
if not success:
break
img, faces = detector.findFaceMesh(img)
if len(faces) != 0:
print(faces[0])
cTime = time.time()
fps = 1 / (cTime - pTime)
pTime = cTime
cv2.putText(img, f'FPS: {int(fps)}', (20, 70), cv2.FONT_HERSHEY_PLAIN,
3, (0, 255, 0), 3)
cv2.imshow("Image", img)
cv2.waitKey(1)
if __name__ == "__main__":
main()
参考目录
https://google.github.io/mediapipe/solutions/face_mesh.html