顔の向きを求めてみた

# はじめに
この記事は、[SLP KBIT Advent Calendar 2022](https://adventar.org/calendars/7569)の23日目の記事になります。
この記事ではカメラから取得した画像をもとに、dlibやnumpy、openCVを用いて顔の向きを求めます。顔の向きは、線で示しており角度を求めるには追加で計算を行う必要があります。

# 開発環境
```
openCV 4.6.0.66
numpy 1.23.1
dlib 19.24.0
```

# 解説
コードの要点のみ解説します。

ここでは、カメラの姿勢を計算しています。solvePnPではカメラの位置、姿勢を３次元の座標と２次元の座標の対応から求めれます。
```
success, vector_rotation, vector_translation = cv2.solvePnP(figure_points_3D, image_points_2D, matrix_camera, distortion_coeffs, flags=0)
```
projectPointsでは、３次元の点をカメラ視点に投影します。（３次元の点を２次元の点に変換）
```
nose_end_point2D, jacobian = cv2.projectPoints(np.array([(0.0, 0.0, 1000.0)]), vector_rotation, vector_translation, matrix_camera, distortion_coeffs)
```
これは、顔のモデル座標を登録しています。projectPointsで投影する点は、３次元で鼻の頂点の正面になります。従って、顔の向きに伴ってnose_end_point2Dに入る点は移動します。
```
figure_points_3D = 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),    # Left Mouth corner
                        (150.0, -150.0, -125.0)      # Right mouth corner
                    ])
```
point1は鼻の頂点、point2はnose_end_point2Dに入る点です。その２点を結んだ線を描画します。この線が顔の向きを示すものとなります。
```
cv2.line(img, point1, point2, (255,255,255), 2)
```

# コード
```python
import cv2
import numpy as np
import dlib
from imutils import face_utils

WIDTH = 1920
HEIGHT = 1080
face_detector = dlib.get_frontal_face_detector()
predictor_path = 'shape_predictor_68_face_landmarks.dat'
face_predictor = dlib.shape_predictor(predictor_path)
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)

while True:
    ret,img = cap.read()
    img_gry = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
    size = img.shape
    faces = face_detector(img_gry,1)
    for face in faces:
        landmark = face_predictor(img_gry,face)
        landmark = face_utils.shape_to_np(landmark)

    image_points_2D = np.array([
                        (landmark[30]),  # Nose tip
                        (landmark[8]),  # Chin
                        (landmark[45]),  # Left eye corner
                        (landmark[36]),  # Right eye corner
                        (landmark[54]),  # Left mouth
                        (landmark[48])   # Right mouth
                      ], dtype="double")

    figure_points_3D = 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 corne
                            (-150.0, -150.0, -125.0),    # Left Mouth corner
                            (150.0, -150.0, -125.0)      # Right mouth corner
                        ])

    distortion_coeffs = np.zeros((4,1))
    focal_length = size[1]
    center = (size[1]/2, size[0]/2)
    matrix_camera = np.array(
                         [[focal_length, 0, center[0]],
                         [0, focal_length, center[1]],
                         [0, 0, 1]], dtype = "double"
                         )
    success, vector_rotation, vector_translation = cv2.solvePnP(figure_points_3D, image_points_2D, matrix_camera, distortion_coeffs, flags=0)
    nose_end_point2D, jacobian = cv2.projectPoints(np.array([(0.0, 0.0, 1000.0)]), vector_rotation, vector_translation, matrix_camera, distortion_coeffs)
    for p in image_points_2D:
        cv2.circle(img, (int(p[0]), int(p[1])), 3, (0,0,255), -1)
    point1 = ( int(image_points_2D[0][0]), int(image_points_2D[0][1]))

    point2 = ( int(nose_end_point2D[0][0][0]), int(nose_end_point2D[0][0][1]))

    cv2.line(img, point1, point2, (255,255,255), 2)

    cv2.imshow("Final",img)

    if cv2.waitKey(1) == 27:
        break
    
cap.release()
cv2.destroyAllWindows()
```
# おわりに
今回は顔の向きを求めるコードの解説を行いました。計算内容を調べるのが大変でしたので、自分が参考にしたサイトを置いておきます。興味がある人はのぞいてみてください。

# 参考
https://learnopencv.com/head-pose-estimation-using-opencv-and-dlib/

http://www.vision.is.tohoku.ac.jp/files/6815/2582/2345/4th.pdf

https://moitkfm.blogspot.com/2014/06/2d-3d.html

https://ossyaritoori.hatenablog.com/entry/2019/03/02/OpenCV%E3%81%A7%E4%BD%BF%E3%82%8F%E3%82%8C%E3%82%8B%E5%BA%A7%E6%A8%99%E7%B3%BB%E3%81%AE%E4%BD%9C%E6%B3%95%E3%83%A1%E3%83%A2