File size: 18,896 Bytes
d5d20be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
"""
@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)  # 结果