OpenCVで顔向き推定を行う

#1 はじめに
これは、OpenCV Advent Calendar 2016 7日目の記事です。関連記事は[目次](http://qiita.com/advent-calendar/2016/opencv)にまとめられています。

本記事は、インタフェースや、[運転支援でのドライバーのモニター](http://www.automobilemag.com/news/volvo-driver-state-estimation-monitoring-sensors/)などに有効とされている顔向き推定をOpenCVで行うための記事です。

タイトルにOpenCVを使用した顔向き推定と書いていますが、残念ながらOpenCVだけでは、顔向き推定を行うには無理があります。特に顔の特徴点の検出には、現在手軽さとその精度において、別のCVライブラリである[Dlib](http://dlib.net/)を用いる必要があります。また、顔の３次元モデル上での特徴点の３次座標情報は必須であり、推定精度向上のためには、撮影に使用したカメラの内部パラメータも必要になります。今回は、顔を特徴点と３次元モデル情報を入手できていることを前提とし、話を進めます。

今回の[元記事はこちら](http://www.learnopencv.com/head-pose-estimation-using-opencv-and-dlib/)です。本記事では、プログラムの実行に必要な項目に絞り記載しているため、詳細な説明が必要な場合は、元記事を参照してほしい。Dlibを使用することで、カメラ画像からの画像に対し[リアルタイムで顔向き推定](https://www.youtube.com/watch?v=NIgKWr4OxoE)を行うことができるようになります。

### CVにおける顔向き推定
正しくは、頭部姿勢推定（Head Pose Estimation）ですが、わかりやすく顔向き推定としています。CVでは、カメラに対し頭部の相対的な移動（平行移動と回転移動）を意味しており、頭部の移動、あるいは、カメラの移動により求める数字は変化します。

平行移動は３次元座標内での、座標のX,Y,Z軸での移動である。回転移動は、頭部に対しYow,Pitch,Lawと呼ばれる軸を設け、その軸を中心とした、回転を意味します。[説明図](https://www.researchgate.net/figure/279291928_fig1_Fig-1-Orientation-of-the-head-in-terms-of-pitch-roll-and-yaw-movements-describing)はこちらです。

### 顔の特徴点検出
顔の特徴点は、英語では、facial landmarkと呼ばれています。OpenCVには、目、鼻、口の特徴点（正しくは領域）の検出用に、の専用辞書が準備されています。それらを使用することで４点（左目の中心点、右目の中心点、鼻の中心点、口の中心点）の座標しか得ることができません。あとで述べますが、必要とするパラメータを推定するためには、最低6点の座標が必要となるため、OpneCVでは、専用に辞書を作成しない限り、6点の座標を得ることができません。Dlibは、顔の特徴点を検出するための専用の辞書をサンプルプログラムが準備されており、それらを使用することで必要とする6点の座標を得ることができますが、OpenCVとは関係ないため説明を省略します。

# 2 計算仕方
３次元から２次元への射影変換が基本となり、VRやARでは馴染みのDLT（Direct Linear Transform）が出てきますが、詳しくないので説明は省きます。

平行移動と回転移動の６つのパラメータを求めるために、最低でも６点の点が必要になります。今回は、左目端点、右目端点、鼻頂点、口左端、口右端、顎の先の６つの点を使用します。具体的のどこの場所かは、[原文の説明図](http://www.learnopencv.com/wp-content/uploads/2016/09/pose-estimation-requirements-opencv.jpg)を見てください。

これらの６点の３次元座標上での座標と２次元上での座標とカメラパラメータを引数とし、おなじみのcv::solvePnP 関数を使用し、回転ベクトルと、平行移動ベクトルを求めるます。

## 2.1 原文のc++サンプルの簡単解説
原文でのサンプルプログラムのポイントのみ説明します。

・std::vector <cv::Point2d> image_points　に　２次元画像上６点（Dlibで取得）の座標を設定しています

```
    image_points.push_back( cv::Point2d(359, 391) );  // 鼻先
    image_points.push_back( cv::Point2d(399, 561) );  // 顎
    image_points.push_back( cv::Point2d(337, 297) );　// 左目端点
    image_points.push_back( cv::Point2d(513, 301) );  // 右目端点
    image_points.push_back( cv::Point2d(345, 465) );  // 口左端
    image_points.push_back( cv::Point2d(453, 469) );  // 口右端
```

・std::vector <cv::Point3d> model_points　に　３次元モデルの６点の座標を設定しています

```
    model_points.push_back(cv::Point3d(0.0f, 0.0f, 0.0f));               // 鼻先
    model_points.push_back(cv::Point3d(0.0f, -330.0f, -65.0f));          // 顎
    model_points.push_back(cv::Point3d(-225.0f, 170.0f, -135.0f));       // 左目端点
    model_points.push_back(cv::Point3d(225.0f, 170.0f, -135.0f));        // 右目端点
    model_points.push_back(cv::Point3d(-150.0f, -150.0f, -125.0f));      // 口左端
    model_points.push_back(cv::Point3d(150.0f, -150.0f, -125.0f));       // 口右端
```

・カメラパラメータを設定。正しいパラメータではなく、ひずみのない理想モデルを設定しています。

```
    double focal_length = im.cols;
    Point2d center = cv::Point2d(im.cols/2,im.rows/2);
    cv::Mat camera_matrix = (cv::Mat_<double>(3,3) << focal_length, 0, center.x, 0 , focal_length, center.y, 0, 0, 1);
    cv::Mat dist_coeffs = cv::Mat::zeros(4,1,cv::DataType<double>::type); // 歪なし
```

・cv::solvePnPによる推定
　結果は、cv::Mat rotation_vectorとtranslation_vectorに格納されます

```
    cv::solvePnP(model_points, image_points, camera_matrix, dist_coeffs, rotation_vector, translation_vector);
```
・鼻の法線の端点を２次元上に射影した座標を計算

```
    projectPoints(nose_end_point3D, rotation_vector, translation_vector, camera_matrix, dist_coeffs, nose_end_point2D);
```


・顔向きを意味する直線を描画

```
    cv::line(im,image_points[0], nose_end_point2D[0], cv::Scalar(255,0,0), 2);
```

# 3 回転角度の取得
Yaw, Pitch , Roll それぞれ何度回転しているのか取得するには、以下の処理を追加する必要があります。

### 3.1 回転ベクトルから回転行列への変換
[Rodrigues](http://docs.opencv.org/2.4/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html#rodrigues)を使用します。これにより3x1のベクトルから3x3の行列に変換されます。

```
    cv::Rodrigues(rotation_vector, rotation_matrix);
```
### 3.2 行列の拡大 3x3から 3x4に

```
    double* _r = rotCamerMatrix.ptr<double>();
    double projMatrix[12] = {_r[0],_r[1],_r[2],0,_r[3],_r[4],_r[5],0,_r[6],_r[7],_r[8],0};
    projMat = cv::Mat(3,4,CV_64FC1,projMatrix);
```
### 3.3 オイラー角への変換
[decomposeProjectionMatrix](http://docs.opencv.org/2.4/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html#decomposeprojectionmatrix)を使用します。最後のeulerAnglesに必要とする角度情報が格納されています。


```
	decomposeProjectionMatrix(projMat, cameraMatrix, rotation_matrix, translation_vector, 
	rotMatrixX, rotMatrixY, rotMatrixZ, eulerAngles)
```
求める角度は、以下の用になります。indexの番号の並びに注意

```
    yaw   = eulerAngles[1]; 
    pitch = eulerAngles[0];
    roll  = eulerAngles[2];
```
###3.4 動作結果
　動作環境　Windows7 64bit + OpenCV 2.4.13.1
　[使用した画像](http://www.learnopencv.com/wp-content/uploads/2016/09/headPose.jpg)は、原文で用いられているものです。サンプル同様に特徴点とガイド線を描画しています。
　コンソールに、角度の値を表示しているのは、サンプルに追加した部分です。
	
　　![adpic.PNG](https://qiita-image-store.s3.amazonaws.com/0/105738/d5617a03-dab0-d518-95d6-2b8fc676b0b0.png)

# 4 おわりに
手抜き感満載の記事にまりました。実は、rotation_vectorから角度の計算の仕方がわからず、苦しみました。OpenCVで顔向き推定を行うやり方を見つけたのは、数時間前であったため急いで文章を書きました。サンプルプログラムは、動作していますが、本当の値(ground truth)がわからないため、正しい値が得られているか今ひとつ確証が得られていません。いずれ追加の情報を[Blog](https://iwaki2009.blogspot.jp/)に記載する予定です。

明日は、negi111111さんのOpenCV DNNのiOSでの利用とその比較についてです。

以上