一张图可以估计“现在偏向哪边、嘴是否张开、眼睛是否闭合”。“刚完成一次转头”或“眨了一次眼”包含时间变化,需要连续图像。

1. 先得到关键点

关键点网络预测眼角、鼻尖、嘴角等位置。常见输出是图像坐标,MediaPipe Face Landmarker 还提供人脸网格及变换矩阵。

若坐标归一化到图像宽高,先还原成像素:

x_pixel = landmark.x * image_width
y_pixel = landmark.y * image_height

不能直接把归一化的 x、y 当作同单位距离:在非正方形图片中,横向 0.1 和纵向 0.1 对应的像素长度不同。网格中的 z 也是模型估计量,不能当作深度传感器实测距离。MediaPipe 说明

2. 用鼻子位置粗估偏转

只有二维点时,可以观察鼻尖相对双眼中点的横向偏移。设左右眼中心为 L、R,鼻尖为 N:

q=Nx−(Lx+Rx)/2∥L−R∥2q=\frac{N_x-(L_x+R_x)/2}{\|L-R\|_2}

q 能作为粗略偏转指标。除以眼距后,图片等比放大不会改变结果;但脸型、透视、歪头都会影响它,不能把 q×某个常数 当作通用角度。

更好的二维实现会先修正画面内倾斜,再计算鼻尖相对眼睛连线的投影。需要角度时,通常采用三维模板加 PnP,或使用姿态网络。

3. 用 PnP 求头部旋转

准备一组标准三维脸点 P_i,以及图上与它们对应的二维点 p_i,再给出相机内参 K、畸变参数。透视投影关系为:

λi[uivi1]=K(RPi+t)\lambda_i \begin{bmatrix}u_i\\v_i\\1\end{bmatrix} =K(RP_i+t)

solvePnP 寻找旋转 R 和平移 t,使投影点尽量贴近观察点。三维点和二维点的顺序必须一致,不能把右眼角对应成左嘴角。

ok, rvec, tvec = cv2.solvePnP(points_3d, points_2d, K, distortion)
if not ok:
raise ValueError("姿态求解失败")
R, _ = cv2.Rodrigues(rvec)

rvec 是旋转向量,不能把其中某一项直接当作 yaw。先转成旋转矩阵,再按约定分解。OpenCV PnP 文档

本文采用 R = Rz(roll) Ry(yaw) Rx(pitch) 的旋转顺序,在非万向节锁区域:

yaw = math.degrees(math.atan2(
-R[2, 0], math.hypot(R[0, 0], R[1, 0])
))

这个公式与旋转顺序绑定。atan2(R[0,2], R[2,2]) 在纯 Y 轴旋转时可给出相同值,但在混合转动时不能直接套用。

相机内参最好经标定获得;用图像宽度猜焦距、用通用脸模板,都只能得到近似姿态。把三维点重新投影到图上,可以检查平均重投影误差,但低误差不代表模板和相机参数就一定真实。

可运行的 pnp_pose.py 用已知 25° 旋转合成二维点,再反求旋转。它验证计算与坐标约定,不测真人角度精度。

4. 左右怎样定义

图像通常 x 向右、y 向下。PnP 的相机坐标、三维脸模板坐标、MediaPipe 的坐标约定不一定相同。

先让受试者面对相机,再明确做一次“本人左转”,观察实际角度的符号,才给正负值贴上左右标签。自拍预览还可能镜像:只翻转显示与翻转算法输入是两件事。

识别小幅变化可以使用 Δyaw = yaw - yaw_neutral,减少起始姿态的偏差。这个基线修正不解决错误的坐标约定。

5. 张嘴与闭眼怎样量化

嘴的开合可以用内唇间距除以嘴角距离:

M=∥pupper−plower∥2∥pleft−pright∥2M=\frac{\|p_{\mathrm{upper}}-p_{\mathrm{lower}}\|_2} {\|p_{\mathrm{left}}-p_{\mathrm{right}}\|_2}

例如嘴宽 50 像素、内唇间距 10 像素,M=0.2。等比放大后结果不变。不同脸型、表情和侧脸会改变比例,不存在适合所有人的固定张嘴阈值。

闭眼常用六个眼轮廓点计算 EAR。按眼周顺序编号 p1…p6,p1、p4 为两侧眼角:

EAR=∥p2−p6∥2+∥p3−p5∥22∥p1−p4∥2EAR=\frac{\|p_2-p_6\|_2+\|p_3-p_5\|_2}{2\|p_1-p_4\|_2}

眼睛闭合时,两条竖向距离减小,EAR 通常下降。眼角点取错、镜片遮挡、脸部侧转都会影响结果。这个比例描述当前开合,完整眨眼需要观察“开→闭→开”。EAR 原始方法

def eye_aspect_ratio(p):
p1, p2, p3, p4, p5, p6 = p
width = math.dist(p1, p4)
if width <= 1e-8:
raise ValueError("眼角重合,不能计算")
return (math.dist(p2, p6) + math.dist(p3, p5)) / (2 * width)

6. 姿态怎样变成动作

连续帧里,可以跟踪“正脸→目标角度→回正”,并要求每阶段持续若干帧,过滤单帧抖动。指数平滑也是一个简单选择:y_t = αx_t + (1-α)y_(t-1);减小 α 会更平滑,也会增加响应延迟。

单张图只提供一个姿态值,无法验证上述顺序。录像里的人同样能转头、张嘴,所以动作完成也不能代替单图防伪的分类依据。

几何函数与图片模型的运行方式见算法示例。