前言
人脸姿态估计是指通过计算机视觉技术对人脸在三维空间中的旋转和平移进行估计。它涉及到识别人脸的朝向、头部的旋转角度和平移距离等信息。准确的人脸姿态估计可以为许多应用场景提供关键的信息,例如头部姿态分析、表情识别、姿势跟踪等。在本文中我将逐步从基础原理到代码组成为大家带来讲解,其中代码部分是重点,原理部分会给大家附上相关链接参考。
原理简介
人脸姿态估计旨在通过计算机视觉技术获取人脸在三维空间中的旋转和平移信息。它涉及到人脸的朝向、头部的旋转角度和平移距离等。在人脸姿态估计中主要有如下两种方法进行(本文主要讲解的是方法二):
【方法一】: 基于深度学习的方法:
基于深度学习的方法:基于深度学习的方法利用卷积神经网络(CNN)或循环神经网络(RNN)等深度学习模型,直接从人脸图像中学习姿态估计。这些方法能够学习到更复杂的特征表示,并在大规模数据集上取得出色的性能。
【方法二】: 基于2D标定信息估计3D姿态信息(计算机视觉PnP问题):
- 特征点定位:人脸姿态估计的第一步是通过特征点定位来检测和定位人脸的关键点,如眼睛、鼻子、嘴巴等。这些关键点提供了人脸的局部结构信息,可以用于后续的姿态估计。
- 旋转表示:常见的旋转表示方法包括欧拉角(Euler angles)和旋转矩阵(rotation matrix)。欧拉角通过三个旋转角度(通常是俯仰角、偏航角和翻滚角)来描述头部的旋转姿态。旋转矩阵则是一个3x3的矩阵,表示头部从一个坐标系旋转到另一个坐标系的变换。
- 3D模型重建:基于特征点的定位结果,可以使用3D人脸模型进行姿态估计。通过将人脸的2D图像映射到3D模型上,可以估计出人脸的旋转和平移信息。这需要先建立一个人脸的3D模型,然后通过优化方法将模型与特征点对齐,从而得到姿态估计结果。
特征点定位
特征点定位是用来检测人脸部分的关键部位,基础的有五官,也有其他更多的特征点表示,大家可以参考我的上一篇文章中所述的特征点检测方案【实操:人脸矫正】两次定位操作解决人脸矫正问题,对该篇博客中的检测关键点的代码稍加改动,坐标转换部分见上篇博客
shell
复制代码
def get_face_info(image): img_copy = image.copy() image.flags.writeable = False image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results = face_detection.process(image) # Draw the face detection annotations on the image. image.flags.writeable = True image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) box_info, facial = None, None if results.detections: for detection in results.detections: mp_drawing.draw_detection(image, detection) facial = detection.location_data.relative_keypoints return facial
在上述代码中返回的数据为五官(6个关键点的坐标),这是使用mediapipe 库实现的,下面我们可以尝试使用另一个库实现:dlib。
dlib使用
在Python中使用Dlib库实现人脸关键点检测的步骤如下:
- 确保已经安装了Dlib库,可以使用以下命令进行安装:pip install dlib
- 导入必要的库:
- 加载Dlib的人脸检测器和关键点检测器模型:
- 读取图像并进行灰度化处理:
- 使用人脸检测器检测图像中的人脸:
- 遍历检测到的人脸,并使用关键点检测器检测人脸关键点:
- 显示绘制了关键点的图像:
在上下面代码中添加了后续需要返回的关键点坐标的参数landmarks_part
shell
复制代码
import dlib import cv2 detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") image = cv2.imread("person.jpg") gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) faces = detector(gray) landmarks_part = [] for face in faces: landmarks = predictor(gray, face) # 遍历关键点,并在图像上绘制出来 for n in range(0, 68): x = landmarks.part(n).x y = landmarks.part(n).y landmarks_part.append((x, y)) cv2.circle(image, (x, y), 2, (0, 255, 0), -1) cv2.imshow("Facial Landmarks", image) cv2.waitKey(0)
旋转表示
旋转表示是一种常用的方式来描述头部的旋转姿态。旋转表示方法有多种,其中两种常见的方法是欧拉角(Euler angles)和旋转矩阵(rotation matrix),在本文中着重讲解欧拉角:
欧拉角是一种通过三个旋转角度来描述物体的旋转姿态的方法。在人脸姿态估计中,通常使用俯仰角(pitch)、偏航角(yaw)和翻滚角(roll)来表示头部的旋转姿态。
- 俯仰角:表示头部绕着垂直于地面的X轴旋转的角度。当头部向下低头时,俯仰角为正值;当头部向上仰头时,俯仰角为负值。
- 偏航角:表示头部绕着垂直于地面的Y轴旋转的角度。当头部向右转时,偏航角为正值;当头部向左转时,偏航角为负值。
- 翻滚角:表示头部绕着垂直于地面的Z轴旋转的角度。当头部向右倾斜时,翻滚角为正值;当头部向左倾斜时,翻滚角为负值。
3D模型重建
进行3D模型重建需要有两个大前提,这里我们以单目相机为例:
【条件一:】 有一个已经校准了的相机(也就是知道相机的内参);
【条件二:】 我们知道物体上的N个3D 点的位置和这些3D点在图像中相应的2D投影。
对于【条件一】大家可以参考我的【专栏: 单双目测距】中有介绍。在这里我将略过。
shell
复制代码
'相机内参矩阵' img_size = (640, 480) focal_length = img_size[0] camera_center = (img_size[1] / 2, img_size[0] / 2) cam_matrix = np.array([[2049.608299, 1.241852862, 1032.391255], [0, 2066.791362, 550.6131349], [0, 0, 1]], dtype="double") '畸变矩阵' dist_coeffs = np.array([0.108221558, -0.232697802, 0.002050653, -0.004714754, 0])
对于【条件二】中的3D坐标和 Coimbra 大学科学技术学院提供的通用三维人脸模型的坐标一致。
shell
复制代码
'头部三维通用模型关键点坐标' object_pts_6 = np.array([ (0.0, 0.0, 0.0), # Nose tip 34 (0.0, -330.0, -65.0), # Chin 9 (-225.0, 170.0, -135.0), # Left eye left corner 46 (225.0, 170.0, -135.0), # Right eye right corne 37 (-150.0, -150.0, -125.0), # Left Mouth corner 55 (150.0, -150.0, -125.0) # Right mouth corner 49 ], dtype=float) / 4.5 object_pts_14 = np.float32([[6.825897, 6.760612, 4.402142], [1.330353, 7.122144, 6.903745], [-1.330353, 7.122144, 6.903745], [-6.825897, 6.760612, 4.402142], [5.311432, 5.485328, 3.987654], [1.789930, 5.393625, 4.413414], [-1.789930, 5.393625, 4.413414], [-5.311432, 5.485328, 3.987654], [2.005628, 1.409845, 6.165652], [-2.005628, 1.409845, 6.165652], [2.774015, -2.080775, 5.048531], [-2.774015, -2.080775, 5.048531], [0.000000, -3.116408, 6.097667], [0.000000, -7.415691, 4.070434]]) object_pts_68 = np.array([ [-73.393523, -29.801432, -47.667532], [-72.775014, -10.949766, -45.909403], [-70.533638, 7.929818, -44.84258 ], [-66.850058, 26.07428 , -43.141114], [-59.790187, 42.56439 , -38.635298], [-48.368973, 56.48108 , -30.750622], [-34.121101, 67.246992, -18.456453], [-17.875411, 75.056892, -3.609035], [ 0.098749, 77.061286, 0.881698], [ 17.477031, 74.758448, -5.181201], [ 32.648966, 66.929021, -19.176563], [ 46.372358, 56.311389, -30.77057 ], [ 57.34348 , 42.419126, -37.628629], [ 64.388482, 25.45588 , -40.886309], [ 68.212038, 6.990805, -42.281449], [ 70.486405, -11.666193, -44.142567], [ 71.375822, -30.365191, -47.140426], [-61.119406, -49.361602, -14.254422], [-51.287588, -58.769795, -7.268147], [-37.8048 , -61.996155, -0.442051], [-24.022754, -61.033399, 6.606501], [-11.635713, -56.686759, 11.967398], [ 12.056636, -57.391033, 12.051204], [ 25.106256, -61.902186, 7.315098], [ 38.338588, -62.777713, 1.022953], [ 51.191007, -59.302347, -5.349435], [ 60.053851, -50.190255, -11.615746], [ 0.65394 , -42.19379 , 13.380835], [ 0.804809, -30.993721, 21.150853], [ 0.992204, -19.944596, 29.284036], [ 1.226783, -8.414541, 36.94806 ], [-14.772472, 2.598255, 20.132003], [ -7.180239, 4.751589, 23.536684], [ 0.55592 , 6.5629 , 25.944448], [ 8.272499, 4.661005, 23.695741], [ 15.214351, 2.643046, 20.858157], [-46.04729 , -37.471411, -7.037989], [-37.674688, -42.73051 , -3.021217], [-27.883856, -42.711517, -1.353629], [-19.648268, -36.754742, 0.111088], [-28.272965, -35.134493, 0.147273], [-38.082418, -34.919043, -1.476612], [ 19.265868, -37.032306, 0.665746], [ 27.894191, -43.342445, -0.24766 ], [ 37.437529, -43.110822, -1.696435], [ 45.170805, -38.086515, -4.894163], [ 38.196454, -35.532024, -0.282961], [ 28.764989, -35.484289, 1.172675], [-28.916267, 28.612716, 2.24031 ], [-17.533194, 22.172187, 15.934335], [ -6.68459 , 19.029051, 22.611355], [ 0.381001, 20.721118, 23.748437], [ 8.375443, 19.03546 , 22.721995], [ 18.876618, 22.394109, 15.610679], [ 28.794412, 28.079924, 3.217393], [ 19.057574, 36.298248, 14.987997], [ 8.956375, 39.634575, 22.554245], [ 0.381549, 40.395647, 23.591626], [ -7.428895, 39.836405, 22.406106], [-18.160634, 36.677899, 15.121907], [-24.37749 , 28.677771, 4.785684], [ -6.897633, 25.475976, 20.893742], [ 0.340663, 26.014269, 22.220479], [ 8.444722, 25.326198, 21.02552 ], [ 24.474473, 28.323008, 5.712776], [ 8.449166, 30.596216, 20.671489], [ 0.205322, 31.408738, 21.90367 ], [ -7.198266, 30.844876, 20.328022]]) reprojectsrc = np.float32 ([[10.0, 10.0, 10.0], [10.0, -10.0, 10.0], [-10.0, 10.0, 10.0], [-10.0, -10.0, 10.0]])
获取了前置的两个条件后,我们可使用如下代码实现对获取到的关键点进行3D重构。 步骤如下:
- 图像坐标系中点的坐标从face_landmark_localization的检测结果抽取姿态估计需要的点坐标;
- 函数solvepnp接收一组对应的3D坐标和2D坐标,以及相机内参camera_matrix和dist_coeffs进行反推图片的外参;
- 函数projectPoints根据所给的3D坐标和已知的几何变换来求解投影后的2D坐标;
shell
复制代码
def get_head_pose(landmarks_part,point_number): '''即图像坐标系中点的坐标从face_landmark_localization的检测结果抽取姿态估计需要的点坐标''' if point_number==14: image_pts = np.float32([landmarks_part[17], landmarks_part[21], landmarks_part[22], landmarks_part[26], landmarks_part[36], landmarks_part[39], landmarks_part[42], landmarks_part[45], landmarks_part[31], landmarks_part[35], landmarks_part[48], landmarks_part[54], landmarks_part[57], landmarks_part[8]]) '函数solvepnp接收一组对应的3D坐标和2D坐标,以及相机内参camera_matrix和dist_coeffs进行反推图片的外参' _, rotation_vec, translation_vec = cv2.solvePnP(object_pts_14, image_pts, cam_matrix, dist_coeffs) elif point_number==6: image_pts = np.float32([landmarks_part[34], landmarks_part[9], landmarks_part[46], landmarks_part[37], landmarks_part[55], landmarks_part[49]]) _, rotation_vec, translation_vec = cv2.solvePnP(object_pts_6, image_pts, cam_matrix, dist_coeffs) else: image_pts = np.float32([landmarks_part]) _, rotation_vec, translation_vec = cv2.solvePnP(object_pts_68, image_pts, cam_matrix, dist_coeffs) '函数projectPoints根据所给的3D坐标和已知的几何变换来求解投影后的2D坐标' reprojectdst, _ = cv2.projectPoints(reprojectsrc, rotation_vec, translation_vec, cam_matrix, dist_coeffs) reprojectdst = tuple(map(tuple, reprojectdst.reshape(4, 2))) # calc euler angle rotation_mat, _ = cv2.Rodrigues(rotation_vec) pose_mat = cv2.hconcat((rotation_mat, translation_vec)) _, _, _, _, _, _, euler_angle = cv2.decomposeProjectionMatrix(pose_mat) return reprojectdst, euler_angle
姿态估计
上述的几大关键步骤函数已完成部署,后面需要串联起来运行,这里我们梳理一下思路:
def angle_xyz(point_x, point_y, point_z): """ 1. X 抬头 、 低头 2. Y 头左转 、 头右转 3. Z 头左斜 、 头右斜 """ if point_x > 5: point_x_status = "down" elif point_x < -5: point_x_status = "up" else: point_x_status = " " if point_y > 5: point_y_status = "right" elif point_y < -5: point_y_status = "left" else: point_y_status = " " if point_z > 5: point_z_status = "left" elif point_z < -5: point_z_status = "right" else: point_z_status = " " return point_x_status, point_y_status, point_z_status if __name__ == "__main__": cap = cv2.VideoCapture(0) while cap.isOpened(): success, src_img = cap.read() if not success: print("Ignoring empty camera frame.") continue else: landmarks_part = draw_landmarks(src_img) if len(landmarks_part) == 68: reprojectdst, euler_angle = get_head_pose(landmarks_part, 14) point_x = euler_angle[0][0] point_y = euler_angle[1][0] point_z = euler_angle[2][0] x_status, y_status, z_status = angle_xyz(point_x, point_y, point_z) cv2.putText(src_img, str("X: %.2f Status: %s" % (point_x, x_status)), (5, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 0, 255), 2) cv2.putText(src_img, str('Y: %.2f Status: %s' % (point_y, y_status)), (5, 50), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 0, 255), 2) cv2.putText(src_img, str('Z: %.2f Status: %s' % (point_z, z_status)), (5, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 0, 255), 2) cv2.imshow("src_img", src_img) cv2.waitKey(1)