顔向き推定をOpenCVの機能だけで実現したい!

- この記事は[OpenCV Advent Calendar 2021](https://qiita.com/advent-calendar/2021/opencv)の1日目の記事です。
- 他の記事は目次にまとめられています。


■ 顔向き推定をOpenCVの機能だけで実現したい!
============================================
OpenCVの"CV"とは、Comuter Visionのことである。

:::note info
近年のOpenCVは、Vision以外にもAudioにも手を出していますが……
:::

Commuter Visionにもいろいろな種類の課題がある。

- 対象：人物,  動物(猫, 犬, ...), 機械(車, 飛行機, ...
- 部位：顔 / 目 / 全身 / 上半身 / 下半身
- 目的：検出, 認証, 状態推定, ...
- 手段：機械学習(SVM, DNN, ...)

さて、今回は「顔向き推定の実現」というテーマで、OpenCVのみで行うチュートリアルを作っていきたい。

◯おいおい、顔向き推定なんて皆やってるだろ？と思われるかもしれませんが...
---------------------------------------------------------------------------

顔向き推定について書かれているドキュメントは、確かに少なくないです。

- [Head Pose Estimation using OpenCV and Dlib](https://learnopencv.com/head-pose-estimation-using-opencv-and-dlib/)
- [PythonとOpenCV+dlibを用いた頭部方向推定](https://qiita.com/oozzZZZZ/items/1e68a7572bc5736d474e)
- [webカメラとOpenCV.jsとブラウザでリアルタイム顔向き推定を行う](https://blog.mahoroi.com/posts/2020/05/browser-head-pose-estimation/)


ただ、すぐ使えるかというと、色々難しい。

- 顔向き推定をOpenCV単独で完結できているサンプルが見つからない(DilibやTensorflowなど外部ライブラリ併用が前提)
- 学習済みデータ、モデルデータの入手について省略されている場合もある。

そこで今回は「OpenCV単体だけで完結する、顔向き推定」をまとめたいです。

■ 開発環境
===========
今回は、Ubuntu 21.10上で、OpenCV 4.5.4をベースに検討します。Windows / MacOS環境の方は気合と勘で何とかしてください。

![Ubuntu-21.10](https://img.shields.io/badge/Ubuntu-21.10-brightgreen) 　![OpenCV-4.5.4](https://img.shields.io/badge/OpenCV-4.5.4-brightgreen) 

◯初めに
---------
本テストを行うには、opencvだけでなく、opencv-contribも必要になります。

```plaintext:ファイル構成
work
├ build (下記cmakeコマンドで作成)
├ opencv-4.5.4/modules
└ opencv_contrib/modules
```

```bash:ビルドcommand例
cmake -S opencv-4.5.4 \
      -B build \
      DOPENCV_EXTRA_MODULES_PATH=./opencv_contrib/modules
cd build
make -j 12 && sudo make install -j 12
```

■ モデルデータの入手（しかし、すでに持っているかも）
===========================================================
顔向き推定をするためには、「顔検出」と「landmark抽出」のためにそれぞれのモデルデータを入手する必要がある。しかし、opencvをopencv_contrib付きでコンパイルできている時点で、すでに入手できている（かもしれない）。

(1) haarcascade_frontalface_*.xml
----------------------------------
画像から顔を検出するための学習モデルデータ。このファイルは、opencv/opencvの中に入っている・・・。のだが、あまり普段触らない[data]フォルダのの下にあるので、要注意。

```plaintext
kmtr@kmtr-virtual-machine:~/work$ find ./opencv-4.5.4/ -name "haarcascade_frontalface_*.xml"
./opencv-4.5.4/samples/winrt/FaceDetection/FaceDetection/Assets/haarcascade_frontalface_alt.xml
./opencv-4.5.4/samples/winrt_universal/VideoCaptureXAML/video_capture_xaml/video_capture_xaml.Windows/Assets/haarcascade_frontalface_alt.xml
./opencv-4.5.4/data/haarcascades_cuda/haarcascade_frontalface_alt_tree.xml
./opencv-4.5.4/data/haarcascades_cuda/haarcascade_frontalface_alt2.xml
./opencv-4.5.4/data/haarcascades_cuda/haarcascade_frontalface_alt.xml
./opencv-4.5.4/data/haarcascades_cuda/haarcascade_frontalface_default.xml
./opencv-4.5.4/data/haarcascades/haarcascade_frontalface_alt_tree.xml
./opencv-4.5.4/data/haarcascades/haarcascade_frontalface_alt2.xml
./opencv-4.5.4/data/haarcascades/haarcascade_frontalface_alt.xml
./opencv-4.5.4/data/haarcascades/haarcascade_frontalface_default.xml
```



(2) face_landmark_model.dat
-------------------------------
画像上から目や耳などを検出するための学習モデルデータ。このファイルは、opencv/opencv でも、 opencv/opencv_contribでも配布していない。 実はopencv/opencv_contrib/face/CMakeLists.txtに 当該データをDLするように記載がある（使うときは、ちゃんとライセンスを見てね！）。

> https://github.com/opencv/opencv_3rdparty/tree/contrib_face_alignment_20170818
> https://github.com/opencv/opencv_contrib/blob/4.x/modules/face/CMakeLists.txt

opencv_conttib付でコンパイルすると、opencvのソースコードを入れたフォルダにダウンロードされているかもしれない。

```plaintext
./opencv-4.5.4/.cache/data/7505c44ca4eb54b4ab1e4777cb96ac05-face_landmark_model.dat
```

:::note alert
このファイルは、3条BSDでの配布に見えます。使うときはくれぐれもライセンスのドキュメントを熟読して使ってください。
:::

■ 実行結果
==========
（サンプルコードはこのページの下部に折りたたんであります。）

テストデータは、 "https://github.com/opencv/opencv_contrib/tree/4.x/modules/face/tutorials/face_landmark/images" を使う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/d548c7c5-eff0-79f2-deb6-cfdfc856e068.png)　　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/29e96466-3451-2aca-cbdb-c2d3caf04ed9.png)


■ アルゴリズムの概略
==========================

1. contrib/face moduleを使って、顔のlandmarkの2次元上での座標を入手する
1. 3次元上で、各landmarkの座標を「仮に」決める。
1. 2次元と3次元の座標変換するための、matrixを得る。
1. 仮の3次元座標上で、法線となるベクトルを定義して、これを2次元に置き換える

この時、「鼻」「顎」「左目の左端」「右目の右端」「口の両端」が、landmarkの対象となっている。

◯現状でできること
------------------
とりあえず、あっち向いてホイ！はできますね……

◯もっと頑張れば...
------------------
ここから、更に行列計算をしっかりやれば、３次元空間上での顔の向き推定までできるはず…だけど力尽きたので、今回はここまで。

また、今回の顔のlandmark抽出を使うと、lip syncなんかもできそうな感じですね‥‥　ふむふむ。

■ まとめ：顔向き推定をOpenCVの機能だけで実現できる！
============================================
OpenCVのcontrib/face moduleを使う事で、顔向き推定がOpenCVのみで実現できることを確認できました！

サンプルコードを下記に置きました。


<details><summary>サンプルコード</summary><div>

■ 顔向き推定のプログラム
==========================

◯Makefile
-----------

```makefile
CXX=clang++-13

a.out : main.cpp Makefile
        $(CXX) main.cpp \
                -g -pg \
                -I /usr/local/include/opencv4/ \
                -lopencv_core \
                -lopencv_imgcodecs \
                -lopencv_imgproc \
                -lopencv_objdetect \
                -lopencv_calib3d \
                -lopencv_face
```

```cpp:main.cpp
/*
 * based on https://github.com/opencv/opencv_contrib/blob/4.x/modules/face/samples/sampleDetectLandmarks.cpp
 */

#include "opencv2/face.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/objdetect.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/calib3d.hpp"

#include <iostream>
#include <vector>
#include <string>

using namespace std;
using namespace cv;
using namespace cv::face;

static void inputImage(int argc, char* argv[], Mat& _dst)
{
    // ファイルから画像を読み出す
    Mat img;
    if ( argc == 2 ) {
        img = imread(argv[1]);
    }else{
        // デバッグ時に引数付け忘れると面倒なのでデフォルトの画像を指定。やらなくてもいい。
        img = imread("227943776_1.jpg");
    }
    img.copyTo(_dst);
}

static void outputImage(int argc, char* argv[], Mat& _src)
{
    // ファイルへの出力
    imwrite("result.jpg",_src);
}

static bool myDetector(InputArray image, OutputArray faces, CascadeClassifier *face_cascade)
{
    Mat gray;

    // 画像データを1chに変換する
    if (image.channels() > 1)
    {
        cvtColor(image, gray, COLOR_BGR2GRAY);
    }
    else
    {
        gray = image.getMat().clone();
    }

    // 正規化する
    equalizeHist(gray, gray);

    // 顔領域を検出する
    vector<Rect> faces_;
    face_cascade->detectMultiScale(
        gray,
        faces_,
        1.4,
        2,
        CASCADE_SCALE_IMAGE,
        Size(30, 30)
    );

    Mat(faces_).copyTo(faces);
    return true;
}

int main(int argc, char* argv[] )
{
    // See. https://github.com/opencv/opencv_contrib/blob/4.x/modules/face/samples/sampleDetectLandmarks.cpp
    // 分類器を読み出す
    CascadeClassifier face_cascade;
    // face_cascade.load("haarcascade_frontalface_default.xml");
    face_cascade.load("haarcascade_frontalface_alt2.xml");

    // モデルを読み出す
    FacemarkKazemi::Params params;
    Ptr<FacemarkKazemi> facemark = FacemarkKazemi::create(params);
    facemark->setFaceDetector((FN_FaceDetector)myDetector, &face_cascade);
    facemark->loadModel("face_landmark_model.dat");

    Mat img;
    while(1) {
        // 処理対象画像の読み出し
        inputImage(argc, argv, img);

        // 顔の検出
        vector<Rect> faces;
        facemark->getFaces(img, faces);
        if(faces.size() == 0) {
            cout << "Faces not detected." << endl;
            return -1;
        }

        // 顔のランドマークの検出
        vector< vector<Point2f> > shapes;
        if( !facemark->fit(img, faces, shapes))
        {
            cout << "fit is failed." << endl;
            return -1;
        }

#if 1
        // １画像に複数の顔が検出されているかもしれないが、
        // ここでは最初に見つかった顔だけを対象とする。
        size_t i = 0;
#else
        // 複数の顔検出をする場合はこちらを有効
        for( size_t i = 0 ; i < shapes.size(); i ++ )
#endif
        {
            // See. https://learnopencv.com/head-pose-estimation-using-opencv-and-dlib/
            // See. "Camera Calibration and 3D Reconstruction" in OpenCV Document

            // 3次元空間上の（仮の）座標を定める
            vector<cv::Point3d> model_points;
            model_points.push_back( cv::Point3d(   0.0f,    0.0f,    0.0f) ); // Nose tip
            model_points.push_back( cv::Point3d(   0.0f, -330.0f,  -65.0f) ); // Chin
            model_points.push_back( cv::Point3d(-225.0f,  170.0f, -135.0f) ); // Left eye left corner
            model_points.push_back( cv::Point3d( 225.0f,  170.0f, -135.0f) ); // Right eye right corner
            model_points.push_back( cv::Point3d(-150.0f, -150.0f, -125.0f) ); // Left Mouth corner
            model_points.push_back( cv::Point3d( 150.0f, -150.0f, -125.0f) ); // Right mouth corner

            // 2次元空間上の座標を決める
            // shapesの添え字は+1して入ってくるので、使うときに1減らす
            vector<cv::Point2d> image_points;
            image_points.push_back( shapes[i][31-1] ); // Nose tip
            image_points.push_back( shapes[i][ 9-1] ); // Chin
            image_points.push_back( shapes[i][46-1] ); // Left eve left corner
            image_points.push_back( shapes[i][37-1] ); // Right eve right corner
            image_points.push_back( shapes[i][55-1] ); // Left Mouth corner
            image_points.push_back( shapes[i][49-1] ); // Right Mouth corner

            // カメラの設定
            double  focal_length = img.cols; // Approximate focal length.
            Point2d center       = cv::Point2d(img.cols/2,img.rows/2);
            Mat camera_matrix = (cv::Mat_<double>(3,3) <<
                    focal_length,            0, center.x,
                               0, focal_length, center.y,
                               0,            0,        1);

            Mat dist_coeffs = cv::Mat::zeros(4,1,cv::DataType<double>::type); // Assuming no lens distortion

            // poseの推定
            // 仮の３次元空間と、２次元空間上の座標を変換する行列を算出
            // (rotationとtranslationを算出)

            Mat rotation_vector, translation_vector;
            cv::solvePnP( model_points,      // Input(3D position)
                          image_points,      // Input(2D position)
                          camera_matrix,     // Input
                          dist_coeffs,       // Input
                          rotation_vector,   // Output
                          translation_vector // Output
            );

            // 表示のための法線ベクトルの計算
            // nose tipが(0,0,0) なので、(0,0,1000) まで描画する。
            // これを2次元座標に plot する。

            vector<Point3d> nose_end_point3D;
            nose_end_point3D.push_back(Point3d(0,0,1000.0));
            vector<Point2d> nose_end_point2D;

            projectPoints(nose_end_point3D,   // Input (3D position)
                          rotation_vector,    // Input
                          translation_vector, // Input
                          camera_matrix,      // Input
                          dist_coeffs,        // Input
                          nose_end_point2D);  // Output (2D position)

            /*
             * 計算結果の出力
             */

            // 顔矩形のプロット
            rectangle(img,faces[i],Scalar( 255, 0, 0 ));

            // 特徴点のプロット
            for(auto &shape : shapes[i] )
            {
                circle(img, shape, 1, cv::Scalar(128,128,255) );
            }

            // 今回使った２次元上の特徴点のプロット
            for(auto &image_pt : image_points)
            {
                drawMarker(img, image_pt, Scalar(0,0,255), MARKER_SQUARE, 10 );
            }

            // 法線の表示
            arrowedLine(img,
                        image_points[0],
                        nose_end_point2D[0],
                        cv::Scalar(255,0,0),
                        2);
        }

        // 結果の出力
        outputImage( argc, argv, img );

        // 静止画なので1画像処理で終了
        break;
    }

    return 0;
}
```

</details>