[Day 28] 来做一个人脸互动的程序吧!

在我完成人脸关键点与人脸对齐的学习後,觉得眼睛有点累想要休息 -- 这时一个应用就出来了!

我们每天会接触到"萤幕"的机会有很多:

  • 电视
  • 电脑
  • 手机
  • 平板

而真正让眼睛休息的时间,通常都是准备要就寝了。

适时的还是要让眼睛做一些放松的运动,像是:
眨眼放松
眼球转动
凝视远方
眼部肌肉按摩
...等等等

假设今天有一个App,就像内建的闹钟一样,

会定期跳出提醒你:嘿!你好像用手机有点久了喔?来作一下眼部放松的运动吧!

只需要花个几分钟,

换来长时间用眼的舒缓,

我们就来作一个人脸互动的应用 -- 眼部放松App

(如果想要玩玩的可以到这里看一下安装与执行步骤,我们接下来将一步一步完成这个App)

本文开始

  1. 在专案下分别建立目录:
    - applications
       - easy-eye-app
          - utils
    
  2. 在当前Python环境安装:
    • imutils
    • opencv-contrib-python
    • dlib
  3. 接下来让我们先想一下流程:
    • 开启摄影机
    • 侦测人脸,提示使用者作人脸对齐
    • 辨识人脸关键点,判断眼睛位置
    • 根据眼睛的眨眼与眼球位置做出对应的判断
  4. 根据前一步的流程,我们需要几个方法来帮助我们开发:
    • 侦测人脸
    • 辨识人脸关键点
    • 侦测人脸面对方向

我们就依序开发各个方法吧!

建立公用方法

  1. 打开utils目录,新增一个face_detector.py (内容与之前Dlib MMOD的类似,只是改成class方式):
    # 汇入必要套件
    import ntpath
    import os
    from bz2 import decompress
    from urllib.request import urlretrieve
    
    import cv2
    import dlib
    
    
    class FaceDetector:
        def __init__(self):
            # 下载模型档案(.bz2)与解压缩
            model_name = "mmod_human_face_detector.dat"
            model_path = os.sep.join([ntpath.dirname(ntpath.abspath(__file__)), model_name])
            if not os.path.exists(model_path):
                urlretrieve(f"https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2",
                            model_name + ".bz2")
                with open(model_name, "wb") as new_file, open(model_name + ".bz2", "rb") as file:
                    data = decompress(file.read())
                    new_file.write(data)
                os.remove(model_name + ".bz2")
    
            # 初始化模型
            self._detector = dlib.cnn_face_detection_model_v1(model_path)
    
        def detect(self, img):
            rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    
            results = self._detector(rgb, 1)
            rects = [r.rect for r in results]
            return rects
    
  2. 新增另一个档案landmark_detector.py (内容一样与之前Dlib人脸关键点辨识的类似,只是改成class方式);由於侦测人脸是使用Dlib MMOD方法,辨识人脸关键点就直接拿侦测人脸的Bounding Box来用就好:
    import ntpath
    import os
    from bz2 import decompress
    from urllib.request import urlretrieve
    
    import cv2
    import dlib
    from imutils import face_utils
    
    
    class LandmarkDetector:
        def __init__(self, predictor_type):
            if predictor_type == 5:
                model_url = f"http://dlib.net/files/shape_predictor_5_face_landmarks.dat.bz2"
                model_name = "shape_predictor_5_face_landmarks.dat"
            elif predictor_type == 68:
                model_url = f"https://github.com/davisking/dlib-models/raw/master/shape_predictor_68_face_landmarks_GTX.dat.bz2"
                model_name = "shape_predictor_68_face_landmarks_GTX.dat"
            else:
                raise ValueError(f"un-support predictor type: {predictor_type}, must be 5 or 68!")
    
            model_path = os.sep.join([ntpath.dirname(ntpath.abspath(__file__)), model_name])
            if not os.path.exists(model_path):
                urlretrieve(model_url, model_name + ".bz2")
                with open(model_name, "wb") as new_file, open(model_name + ".bz2", "rb") as file:
                    data = decompress(file.read())
                    new_file.write(data)
                os.remove(model_name + ".bz2")
    
            # 初始化关键点侦测模型
            self._predictor = dlib.shape_predictor(model_path)
    
        def detect(self, img, rects):
            shapes = []
            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            for rect in rects:
                shape = self._predictor(gray, rect)
                shape = face_utils.shape_to_np(shape)
                shapes.append(shape)
            return shapes
    
  3. 最後是侦测人脸方向用来做人脸对齐。
    还记得之前做人脸对齐提到需要用到人脸关键点,但只能解决2D平面的问题吗?
    这里我们一样会用到人脸关键点,但会使用3D人脸模型估计脸部面对方向
    新增一个档案hand_pose_estimator.py
    import numpy as np
    import cv2
    
    # 3D 模型
    model_points = np.array([
        (0.0, 0.0, 0.0),  # 鼻头
        (0.0, -330.0, -65.0),  # 下巴
        (-225.0, 170.0, -135.0),  # 左眼中心
        (225.0, 170.0, -135.0),  # 右眼中心
        (-150.0, -150.0, -125.0),  # 嘴巴左边中心
        (150.0, -150.0, -125.0)  # 嘴巴右边中心
    ])
    
    
    class HeadPoseEstimator:
        def __init__(self, frame_width, frame_height):
            self.frame_width = frame_width
            self.frame_height = frame_height
    
        @staticmethod
        def _get_2d_points(rotation_vector, translation_vector, camera_matrix, dist_coeffs, val):
            point_3d = []
            rear_size = val[0]
            rear_depth = val[1]
            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 = val[2]
            front_depth = val[3]
            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.float).reshape(-1, 3)
    
            # 将3D座标投影到2D平面上
            (point_2d, _) = cv2.projectPoints(point_3d, rotation_vector, translation_vector, camera_matrix, dist_coeffs)
            point_2d = np.int32(point_2d.reshape(-1, 2))
            return point_2d
    
        def _head_pose_points(self, rotation_vector, translation_vector, camera_matrix, dist_coeffs):
            rear_size = 1
            rear_depth = 0
            front_size = self.frame_width
            front_depth = front_size * 2
            val = [rear_size, rear_depth, front_size, front_depth]
            point_2d = self._get_2d_points(rotation_vector, translation_vector, camera_matrix, dist_coeffs, val)
            p1 = point_2d[2]
            p2 = (point_2d[5] + point_2d[8]) // 2
            return p1, p2
    
        def head_pose_estimate(self, shape):
            face_3d_points = np.array([
                shape[33],  # 鼻头
                shape[8],  # 下巴
                shape[36],  # 左眼中心
                shape[45],  # 右眼中心
                shape[48],  # 嘴巴左边中心
                shape[54]  # 嘴巴右边中心
            ], dtype="double")
    
            # 粗估摄影机相关参数
            focal_length = self.frame_width
            center = (self.frame_width / 2, self.frame_height / 2)
            camera_matrix = np.array([[
                focal_length, 0, center[0]],
                [0, focal_length, center[1]],
                [0, 0, 1]], dtype="double")
    
            # 假设摄影机都是已对焦
            dist_coeffs = np.zeros((4, 1))
    
            # 计算旋转与转换矩阵
            (_, rotation_vector, translation_vector) = cv2.solvePnP(
                model_points,
                face_3d_points,
                camera_matrix,
                dist_coeffs,
                flags=cv2.SOLVEPNP_ITERATIVE)
    
            # 将一个"与脸部垂直"的3D座标投影到2D平面上
            (nose_end_point2D, jacobian) = cv2.projectPoints(np.array([0.0, 0.0, 1000.0]), rotation_vector,
                                                             translation_vector, camera_matrix, dist_coeffs)
    
            # 取得投影到2D平面的点 (後面用来计算脸部垂直方向角度)
            vertical_p1 = (int(face_3d_points[0][0]), int(face_3d_points[0][1]))
            vertical_p2 = (int(nose_end_point2D[0][0][0]), int(nose_end_point2D[0][0][1]))
    
            # 取得水平方向角度用的座标
            (horizontal_p1, horizontal_p2) = self._head_pose_points(rotation_vector, translation_vector, camera_matrix, dist_coeffs)
            return vertical_p1, vertical_p2, horizontal_p1, horizontal_p2
    

到这边前置作业已完成,明天我们将测试这些方法是否可以正常使用!


<<:  Day 26. slate × Normalizing × normalizeNode

>>:  Day27 跟着官方文件学习Laravel-Request 生命周期

javascript表单资料处理&验证(DAY22)

这篇文章会介绍如何使用DOM来处理表单的物件存取,以及利用条件判断式来处理表单的验证,像是在上一篇的...

Netlify CMS : 完全就是为了 JAMstack 而设计的 CMS 系统

Netlify CMS 完全就是为了 JAMstack 而设计的 CMS 系统 前面分享了直接使用第...

Day03:浅谈 Git 和 GitHub

Git Git 是一个开源的分布式版本控制系统, 允许我们跟踪档案异动, 最初目的是为更好地管理 L...

[Angular] Day26. Reactive forms (二)

上一篇中介绍了如何使用 FormControl 建立单个表单控制元件,也介绍了如何使用 FormGr...

Flutter学习Day5 Widget StatelessWidget => StatefulWidget 实作

大家安安 晚上好~~ 今天要把专案里的StatelessWidget 更改成为 StatefulWi...