""" @author: cuny @fileName: faceDetection68.py @create_time: 2022/01/03 下午10:20 @introduce: 人脸68关键点检测主文件,以类的形式封装 """ from hivisionai.hyService.cloudService import GetConfig import os import cv2 import dlib import numpy as np local_file = os.path.dirname(__file__) PREDICTOR_PATH = f"{local_file}/weights/shape_predictor_68_face_landmarks.dat" # 关键点检测模型路径 MODULE3D_PATH = f"{local_file}/weights/68_points_3D_model.txt" # 3d的68点配置文件路径 # 定义一个人脸检测错误的错误类 class FaceError(Exception): def __init__(self, err): super().__init__(err) self.err = err def __str__(self): return self.err class FaceConfig68(object): face_area:list = None # 一些其他的参数,在本类中实际没啥用 FACE_POINTS = list(range(17, 68)) # 人脸轮廓点索引 MOUTH_POINTS = list(range(48, 61)) # 嘴巴点索引 RIGHT_BROW_POINTS = list(range(17, 22)) # 右眉毛索引 LEFT_BROW_POINTS = list(range(22, 27)) # 左眉毛索引 RIGHT_EYE_POINTS = list(range(36, 42)) # 右眼索引 LEFT_EYE_POINTS = list(range(42, 48)) # 左眼索引 NOSE_POINTS = list(range(27, 35)) # 鼻子索引 JAW_POINTS = list(range(0, 17)) # 下巴索引 LEFT_FACE = list(range(42, 48)) + list(range(22, 27)) # 左半边脸索引 RIGHT_FACE = list(range(36, 42)) + list(range(17, 22)) # 右半边脸索引 JAW_END = 17 # 下巴结束点 FACE_START = 0 # 人脸识别开始 FACE_END = 68 # 人脸识别结束 # 下面这个是整张脸的mark点,可以用: # for group in self.OVERLAY_POINTS: # cv2.fillConvexPoly(face_mask, cv2.convexHull(dst_matrix[group]), (255, 255, 255)) # 来形成人脸蒙版 OVERLAY_POINTS = [ JAW_POINTS, LEFT_FACE, RIGHT_FACE ] class FaceDetection68(FaceConfig68): """ 人脸68关键点检测主类,当然使用的是dlib开源包 """ def __init__(self, model_path:str=None, default_download:bool=False, *args, **kwargs): # 初始化,检查并下载模型 self.model_path = PREDICTOR_PATH if model_path is None else model_path if not os.path.exists(self.model_path) or default_download: # 下载配置 gc = GetConfig() gc.load_file(cloud_path="weights/shape_predictor_68_face_landmarks.dat", local_path=self.model_path) self.__detector = None self.__predictor = None @property def detector(self): if self.__detector is None: self.__detector = dlib.get_frontal_face_detector() # 获取人脸分类器 return self.__detector @property def predictor(self): if self.__predictor is None: self.__predictor = dlib.shape_predictor(self.model_path) # 输入模型,构建特征提取器 return self.__predictor @staticmethod def draw_face(img:np.ndarray, dets:dlib.rectangles, *args, **kwargs): # 画人脸检测框, 为了一些兼容操作我没有设置默认显示,可以在运行完本函数后将返回值进行self.cv_show() tmp = img.copy() for face in dets: # 左上角(x1,y1),右下角(x2,y2) x1, y1, x2, y2 = face.left(), face.top(), face.right(), face.bottom() # print(x1, y1, x2, y2) cv2.rectangle(tmp, (x1, y1), (x2, y2), (0, 255, 0), 2) return tmp @staticmethod def draw_points(img:np.ndarray, landmarks:np.matrix, if_num:int=False, *args, **kwargs): """ 画人脸关键点, 为了一些兼容操作我没有设置默认显示,可以在运行完本函数后将返回值进行self.cv_show() :param img: 输入的是人脸检测的图,必须是3通道或者灰度图 :param if_num: 是否在画关键点的同时画上编号 :param landmarks: 输入的关键点矩阵信息 """ tmp = img.copy() h, w, c = tmp.shape r = int(h / 100) - 2 if h > w else int(w / 100) - 2 for idx, point in enumerate(landmarks): # 68点的坐标 pos = (point[0, 0], point[0, 1]) # 利用cv2.circle给每个特征点画一个圈,共68个 cv2.circle(tmp, pos, r, color=(0, 0, 255), thickness=-1) # bgr if if_num is True: # 利用cv2.putText输出1-68 font = cv2.FONT_HERSHEY_SIMPLEX cv2.putText(tmp, str(idx + 1), pos, font, 0.8, (0, 0, 255), 1, cv2.LINE_AA) return tmp @staticmethod def resize_image_esp(input_image_, esp=2000): """ 输入: input_path:numpy图片 esp:限制的最大边长 """ # resize函数=>可以让原图压缩到最大边为esp的尺寸(不改变比例) width = input_image_.shape[0] length = input_image_.shape[1] max_num = max(width, length) if max_num > esp: print("Image resizing...") if width == max_num: length = int((esp / width) * length) width = esp else: width = int((esp / length) * width) length = esp print(length, width) im_resize = cv2.resize(input_image_, (length, width), interpolation=cv2.INTER_AREA) return im_resize else: return input_image_ def facesPoints(self, img:np.ndarray, esp:int=None, det_num:int=1,*args, **kwargs): """ :param img: 输入的是人脸检测的图,必须是3通道或者灰度图 :param esp: 如果输入了具体数值,会将图片的最大边长缩放至esp,另一边等比例缩放 :param det_num: 人脸检测的迭代次数, 采样次数越多,越有利于检测到更多的人脸 :return 返回人脸检测框对象dets, 人脸关键点矩阵列表(列表中每个元素为一个人脸的关键点矩阵), 人脸关键点元组列表(列表中每个元素为一个人脸的关键点列表) """ # win = dlib.image_window() # win.clear_overlay() # win.set_image(img) # dlib的人脸检测装置 if esp is not None: img = self.resize_image_esp(input_image_=img, esp=esp) dets = self.detector(img, det_num) # self.draw_face(img, dets) # font_color = "green" if len(dets) == 1 else "red" # dg.debug_print("Number of faces detected: {}".format(len(dets)), font_color=font_color) landmarkList = [] pointsList = [] for d in dets: shape = self.predictor(img, d) landmark = np.matrix([[p.x, p.y] for p in shape.parts()]) landmarkList.append(landmark) point_list = [] for p in landmark.tolist(): point_list.append((p[0], p[1])) pointsList.append(point_list) # dg.debug_print("Key point detection SUCCESS.", font_color="green") return dets, landmarkList, pointsList def facePoints(self, img:np.ndarray, esp:int=None, det_num:int=1, *args, **kwargs): """ 本函数与facesPoints大致类似,主要区别在于本函数默认只能返回一个人脸关键点参数 """ # win = dlib.image_window() # win.clear_overlay() # win.set_image(img) # dlib的人脸检测装置, 参数1表示对图片进行上采样一次,采样次数越多,越有利于检测到更多的人脸 if esp is not None: img = self.resize_image_esp(input_image_=img, esp=esp) dets = self.detector(img, det_num) # self.draw_face(img, dets) font_color = "green" if len(dets) == 1 else "red" # dg.debug_print("Number of faces detected: {}".format(len(dets)), font_color=font_color) if font_color=="red": # 本检测函数必然只能检测出一张人脸 raise FaceError("Face detection error!!!") d = dets[0] # 唯一人脸 shape = self.predictor(img, d) landmark = np.matrix([[p.x, p.y] for p in shape.parts()]) # print("face_landmark:", landmark) # 打印关键点矩阵 # shape = predictor(img, ) # dlib.hit_enter_to_continue() # 返回关键点矩阵,关键点, point_list = [] for p in landmark.tolist(): point_list.append((p[0], p[1])) # dg.debug_print("Key point detection SUCCESS.", font_color="green") # 最后的一个返回参数只会被计算一次,用于标明脸部框的位置 # [人脸框左上角纵坐标(top),左上角横坐标(left),人脸框宽度(width),人脸框高度(height)] return dets, landmark, point_list class PoseEstimator68(object): """ Estimate head pose according to the facial landmarks 本类将实现但输入图的人脸姿态检测 """ def __init__(self, img:np.ndarray, params_path:str=None, default_download:bool=False): self.params_path = MODULE3D_PATH if params_path is None else params_path if not os.path.exists(self.params_path) or default_download: gc = GetConfig() gc.load_file(cloud_path="weights/68_points_3D_model.txt", local_path=self.params_path) h, w, c = img.shape self.size = (h, w) # 3D model points. self.model_points = np.array([ (0.0, 0.0, 0.0), # Nose tip (0.0, -330.0, -65.0), # Chin (-225.0, 170.0, -135.0), # Left eye left corner (225.0, 170.0, -135.0), # Right eye right corner (-150.0, -150.0, -125.0), # Mouth left corner (150.0, -150.0, -125.0) # Mouth right corner ]) / 4.5 self.model_points_68 = self._get_full_model_points() # Camera internals self.focal_length = self.size[1] self.camera_center = (self.size[1] / 2, self.size[0] / 2) self.camera_matrix = np.array( [[self.focal_length, 0, self.camera_center[0]], [0, self.focal_length, self.camera_center[1]], [0, 0, 1]], dtype="double") # Assuming no lens distortion self.dist_coeefs = np.zeros((4, 1)) # Rotation vector and translation vector self.r_vec = np.array([[0.01891013], [0.08560084], [-3.14392813]]) self.t_vec = np.array( [[-14.97821226], [-10.62040383], [-2053.03596872]]) # self.r_vec = None # self.t_vec = None def _get_full_model_points(self): """Get all 68 3D model points from file""" raw_value = [] with open(self.params_path) as file: for line in file: raw_value.append(line) model_points = np.array(raw_value, dtype=np.float32) model_points = np.reshape(model_points, (3, -1)).T # Transform the model into a front view. # model_points[:, 0] *= -1 model_points[:, 1] *= -1 model_points[:, 2] *= -1 return model_points def show_3d_model(self): from matplotlib import pyplot from mpl_toolkits.mplot3d import Axes3D fig = pyplot.figure() ax = Axes3D(fig) x = self.model_points_68[:, 0] y = self.model_points_68[:, 1] z = self.model_points_68[:, 2] ax.scatter(x, y, z) ax.axis('auto') pyplot.xlabel('x') pyplot.ylabel('y') pyplot.show() def solve_pose(self, image_points): """ Solve pose from image points Return (rotation_vector, translation_vector) as pose. """ assert image_points.shape[0] == self.model_points_68.shape[0], "3D points and 2D points should be of same number." (_, rotation_vector, translation_vector) = cv2.solvePnP( self.model_points, image_points, self.camera_matrix, self.dist_coeefs) # (success, rotation_vector, translation_vector) = cv2.solvePnP( # self.model_points, # image_points, # self.camera_matrix, # self.dist_coeefs, # rvec=self.r_vec, # tvec=self.t_vec, # useExtrinsicGuess=True) return rotation_vector, translation_vector def solve_pose_by_68_points(self, image_points): """ Solve pose from all the 68 image points Return (rotation_vector, translation_vector) as pose. """ if self.r_vec is None: (_, rotation_vector, translation_vector) = cv2.solvePnP( self.model_points_68, image_points, self.camera_matrix, self.dist_coeefs) self.r_vec = rotation_vector self.t_vec = translation_vector (_, rotation_vector, translation_vector) = cv2.solvePnP( self.model_points_68, image_points, self.camera_matrix, self.dist_coeefs, rvec=self.r_vec, tvec=self.t_vec, useExtrinsicGuess=True) return rotation_vector, translation_vector # def draw_annotation_box(self, image, rotation_vector, translation_vector, color=(255, 255, 255), line_width=2): # """Draw a 3D box as annotation of pose""" # point_3d = [] # rear_size = 75 # rear_depth = 0 # point_3d.append((-rear_size, -rear_size, rear_depth)) # point_3d.append((-rear_size, rear_size, rear_depth)) # point_3d.append((rear_size, rear_size, rear_depth)) # point_3d.append((rear_size, -rear_size, rear_depth)) # point_3d.append((-rear_size, -rear_size, rear_depth)) # # front_size = 100 # front_depth = 100 # point_3d.append((-front_size, -front_size, front_depth)) # point_3d.append((-front_size, front_size, front_depth)) # point_3d.append((front_size, front_size, front_depth)) # point_3d.append((front_size, -front_size, front_depth)) # point_3d.append((-front_size, -front_size, front_depth)) # point_3d = np.array(point_3d, dtype=np.float64).reshape(-1, 3) # # # Map to 2d image points # (point_2d, _) = cv2.projectPoints(point_3d, # rotation_vector, # translation_vector, # self.camera_matrix, # self.dist_coeefs) # point_2d = np.int32(point_2d.reshape(-1, 2)) # # # Draw all the lines # cv2.polylines(image, [point_2d], True, color, line_width, cv2.LINE_AA) # cv2.line(image, tuple(point_2d[1]), tuple( # point_2d[6]), color, line_width, cv2.LINE_AA) # cv2.line(image, tuple(point_2d[2]), tuple( # point_2d[7]), color, line_width, cv2.LINE_AA) # cv2.line(image, tuple(point_2d[3]), tuple( # point_2d[8]), color, line_width, cv2.LINE_AA) # # def draw_axis(self, img, R, t): # points = np.float32( # [[30, 0, 0], [0, 30, 0], [0, 0, 30], [0, 0, 0]]).reshape(-1, 3) # # axisPoints, _ = cv2.projectPoints( # points, R, t, self.camera_matrix, self.dist_coeefs) # # img = cv2.line(img, tuple(axisPoints[3].ravel()), tuple( # axisPoints[0].ravel()), (255, 0, 0), 3) # img = cv2.line(img, tuple(axisPoints[3].ravel()), tuple( # axisPoints[1].ravel()), (0, 255, 0), 3) # img = cv2.line(img, tuple(axisPoints[3].ravel()), tuple( # axisPoints[2].ravel()), (0, 0, 255), 3) def draw_axes(self, img, R, t): """ OX is drawn in red, OY in green and OZ in blue. """ return cv2.drawFrameAxes(img, self.camera_matrix, self.dist_coeefs, R, t, 30) @staticmethod def get_pose_marks(marks): """Get marks ready for pose estimation from 68 marks""" pose_marks = [marks[30], marks[8], marks[36], marks[45], marks[48], marks[54]] return pose_marks @staticmethod def rot_params_rm(R): from math import pi,atan2,asin, fabs # x轴 pitch = (180 * atan2(-R[2][1], R[2][2]) / pi) f = (0 > pitch) - (0 < pitch) pitch = f * (180 - fabs(pitch)) # y轴 yaw = -(180 * asin(R[2][0]) / pi) # z轴 roll = (180 * atan2(-R[1][0], R[0][0]) / pi) f = (0 > roll) - (0 < roll) roll = f * (180 - fabs(roll)) if not fabs(roll) < 90.0: roll = f * (180 - fabs(roll)) rot_params = [pitch, yaw, roll] return rot_params @staticmethod def rot_params_rv(rvec_): from math import pi, atan2, asin, fabs R = cv2.Rodrigues(rvec_)[0] # x轴 pitch = (180 * atan2(-R[2][1], R[2][2]) / pi) f = (0 > pitch) - (0 < pitch) pitch = f * (180 - fabs(pitch)) # y轴 yaw = -(180 * asin(R[2][0]) / pi) # z轴 roll = (180 * atan2(-R[1][0], R[0][0]) / pi) f = (0 > roll) - (0 < roll) roll = f * (180 - fabs(roll)) rot_params = [pitch, yaw, roll] return rot_params def imageEulerAngle(self, img_points): # 这里的img_points对应的是facePoints的第三个返回值,注意是facePoints而非facesPoints # 对于facesPoints而言,需要把第三个返回值逐一取出再输入 # 把列表转为矩阵,且编码形式为float64 img_points = np.array(img_points, dtype=np.float64) rvec, tvec = self.solve_pose_by_68_points(img_points) # 旋转向量转旋转矩阵 R = cv2.Rodrigues(rvec)[0] # theta = np.linalg.norm(rvec) # r = rvec / theta # R_ = np.array([[0, -r[2][0], r[1][0]], # [r[2][0], 0, -r[0][0]], # [-r[1][0], r[0][0], 0]]) # R = np.cos(theta) * np.eye(3) + (1 - np.cos(theta)) * r * r.T + np.sin(theta) * R_ # 旋转矩阵转欧拉角 eulerAngle = self.rot_params_rm(R) # 返回一个元组和欧拉角列表 return (rvec, tvec, R), eulerAngle # if __name__ == "__main__": # # 示例 # from hyService.utils import Debug # dg = Debug() # image_input = cv2.imread("./test.jpg") # 读取一张图片, 必须是三通道或者灰度图 # fd68 = FaceDetection68() # 初始化人脸关键点检测类 # dets_, landmark_, point_list_ = fd68.facePoints(image_input) # 输入图片. 检测单张人脸 # # dets_, landmark_, point_list_ = fd68.facesPoints(input_image) # 输入图片. 检测多张人脸 # img = fd68.draw_points(image_input, landmark_) # dg.cv_show(img) # pe = PoseEstimator68(image_input) # _, ea = pe.imageEulerAngle(point_list_) # 输入关键点列表, 如果要使用facesPoints,则输入的是point_list_[i] # print(ea) # 结果