OpenCvSharpとDlibDotNetでやるFace Tracking + α

# はじめに

　この記事は、↓のものの Face Tracking の部分についての解説です。
[FaceRigなんてもう古い！Vをやるならこれを使え!](https://qiita.com/kumaS-kumachan/items/050a90e0fcfd7e9aa3cf)
　Unity上でやっていますが、ほぼC#の領域です。C#の基本的なところ（C言語の基礎レベル）がわからないとたぶんわかんない。（ポインタは除く）
　~~僕の環境ではDlibDotNetの一部分が動かなかったのでちょこっと改造しています。([該当部分](#dlibdotnet-を改造))~~近々、修正が入る予定。
# 環境

* Unity 2018.3.8f1
* OpenCvSharp3 4.1.1(Nuget)
* DlibDotNet 19.17.0.20191202(Nuget)

# OpenCvSharp3・DlibDotNetの基礎

以下、ソースコードでは省略していますが、<br>OpenCvSharp3を使うときは`using OpenCvSharp;`を、<br>DlibDotNetを使うときは`using DlibDotNet;`を<br>以下のようにファイルの先頭に書いてあります。

```csharp
using OpenCvSharp;
using DlibDotNet;

/*  以下ソースコード
    ほげほげ        */
```

### OpenCvSharp3 に画像を読み込む
画像を読み込むサンプルです。

```csharp
Mat mat = new Mat();                                      //空のMat生成
Mat mat2 = new Mat("image.png", ImreadModes.Grayscale);   //画像読み込み
```

Webカメラの場合こうします。

```csharp
Mat image = new Mat();                          //画像を入れる用のMat生成
VideoCapture caputure = new VideoCapture();     //Webカメラを取り扱うためのオブジェクト生成
caputure.Open(0);                               //Webカメラ起動
caputure.Read(image);                           //Webカメラの画像を読み込み
```

ここで出てくる`Mat`というのは2（3）次元配列のクラスで、OpenCvの中核的なクラスで、ここに画像を入れます。細かいところまで書くと、とても長くなるのでここではあまり書きません。困ったら公式リファレンスを見てください。
※公式リファレンス : [OpenCV cv::Mat Reference](https://docs.opencv.org/3.4/d3/d63/classcv_1_1Mat.html)

また、`VideoCapture.Read(Mat)`で取得した`Mat`は、`VideoCapture`側で管理するのが望ましく、手を加えないほうがよいです。手を加えたいときは以下のように **深い** コピーをして、そちらを処理しましょう。

```csharp
Mat mat_copy = image;            // × 浅いコピー
Mat mat_clone = image.Clone();   // 〇 深いコピー
```

※参考 : [OpenCV-CookBook　cv::Matの基本処理](http://opencv.jp/cookbook/opencv_mat.html#id3)

また、`Mat`は勝手には解放されません。このままだと動画を処理しているとメモリを使い果たすので、以下のように使い終わったら開放してやりましょう。なお、`VidoCapture.Read(Mat)`の引数として渡した`Mat`は自動で管理されるので何もしないのが賢明です。（先ほどだと `image` ）

```csharp
//usingパターンで自動的に破棄されるようにする。
using(Mat mat = new Mat()){
    //ほげほげ
}      

Mat mat = new Mat();
mat.Dispose();        //明示的にリソースを破棄する。
```

### DlibDotNet に画像を読み込む

DlibDotNet 自体には直接Webカメラの画像を読み込みできないので、以下のようにして OpenCvSharp の`Mat`を変換して使います。これは正直おまじないだと思っても大丈夫です。

```csharp
    using System.Runtime.InteropServices;         //これいるよ

    Mat mat = new Mat();
    caputure.Read(mat);
    byte array = new byte[mat.Width * mat.Height * mat.ElemSize()];
    Marshal.Copy(mat.Data, array, 0, array.Length);

    Array2D<RgbPixel> image = Dlib.LoadImageData<RgbPixel>(array, (uint)mat.Height, (uint)mat.Width, (uint)(mat.Width * mat.ElemSize()));
    // imageに画像が入ってる
```

※参考 : [DlibDotNet Wiki](https://github.com/takuya-takeuchi/DlibDotNet/wiki/How-to-convert-OpenCVSharp.Mat-to%3F)

# Face Tracking

さて、本題のFace Trackingをする方法を紹介していきますが、その前に画像の前処理についてやっていきます。

## 画像の前処理

### リサイズ

顔を検出するというのは非常に重いです。そこで、画像を小さくしてやることで軽くするというのがこの処理です。なお、顔検出の重さは画像の大きさの2乗（総ピクセル数）に比例します。
このリサイズの重さはほぼないと言っていいぐらいの軽さなので、安心してください。

```csharp
    Mat sourse = new Mat("image.png", ImreadModes.Grayscale);  //元画像
    Mat output = new Mat();                                    //フィルター後の画像
    Cv2.Resize(sourse, output, new Size(320, 180));            //リサイズ
```

（第一引数,　第二引数,　第三引数）は（入力画像,　出力画像,　リサイズ後の大きさ）です。
また、`Mat`クラスは `mat.Cols` で横幅を、 `mat.Rows` で縦の高さを取得できるので、以下のようにして元画像の n分の1の画像を生成できます。

```csharp
    Mat sourse = new Mat("image.png", ImreadModes.Grayscale); 
    Mat output = new Mat(); 
    int n = 2;                                                    // n分の1のn
    Cv2.Resize(sourse, output, new Size(sourse.Cols / n, sourse.Rows / n));
```

### ガウシアンフィルタ・平均化

基本、画像にはノイズがついています。（特に動画だとノイズが大きいです。）そのノイズの影響を少なくするのがこの処理です。なんだか難しそうに聞こえますが、要はこの処理をすると顔の検出が安定するということがわかれば十分です。なお、私は軽量化のためにこの工程を入れていません。

* 平均化　　　　　　　・・・　周りの色の平均を真ん中のピクセルの色にする。
* ガウシアンフィルタ　・・・　上に加えて、中央に近い色ほど重要になるように重み付けする。


```csharp
    Mat sourse = new Mat("image.png", ImreadModes.Grayscale);  //元画像
    Mat output = new Mat();                                    //フィルター後の画像
    Cv2.GaussianBlur(sourse, output, new Size(5, 5), 0);       //ガウシアンフィルタ
    Cv2.Blur(sourse, output, new Size(5, 5));                  //平均化
```
それぞれ、（第一引数,　第二引数,　第三引数）は、（入力画像,　出力画像,　「周りの色」の広さ）です。ガウシアンフィルタの（第四引数）は、（どれだけ近さが重要か）を指定します。ここが0だと自動で入れてくれます。
<font color="red">※注 : 第三引数の`Size()`は中央のピクセルがある必要があるため、奇数しか許されていません。</font>
※ 参考 : [OpenCV Tutorials  画像の平滑化](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_filtering/py_filtering.html)

## Face Tracking

それでは本題の Face Tracking をやっていきます。やるのは顔の検出ですが、動画の毎フレームに適応すると Face Tracking となります。
なお、ここから顔の画像が入った画像が OpenCvSharp3 は `Mat image` 、DlibDotNet は `Array2D<RgbPixel> image` があるものとして使っていきます。

### OpenCvSharp3 の場合

OpenCvSharp3では、カスケード分類機を作成して顔を検出させます。
なお、顔のカスケードファイルは公式が出してるのでそれを使いましょう。
[OpenCVのカスケードファイル in github](https://github.com/opencv/opencv/tree/master/data/haarcascades)

```csharp
    CascadeClassifier cascade = new CascadeClassifier();    // カスケード分類機作成
    cascade.Load("CASCADE_FILE_NAME.xml");                  // カスケード分類機の特徴が書かれたファイルを読み込む  
```
そして、作成したカスケード分類機で顔を検出します。

```csharp
Rect[] faces  = cascade.DetectMultiScale(image);    // これで検出
Rect[] faces2 = cascade.DetectMultiScale(image, 1.1, 3, HaarDetectionType.FindBiggestObject, new Size(10, 10), new Size(300,300));    // こんな感じで設定もできる  
```

引数には、画像が入った `Mat` を、第二引数以降を与えてパラメーターを調整できます。
返り値は、顔が入っていると思われる長方形の **配列** 。`Rect`の中身は以下の通り。

|プロパティ|説明|
|---:|---:|
|X|長方形の左上のx座標。<br>元画像の左上が0で、右向き正。|
|Y|長方形の左上のy座標。<br>元画像の左上が0で、下向き正。|
|Width|長方形の横幅。|
|Height|長方形の縦幅。|

実際に使うときは配列の中身があったり無かったり、複数あったりするので気を付けてください。

さらに詳しく知りたい方は、こちらの記事を見るとよいです。よくまとまっていてわかりやすかったです。
※　参考 : [【入門者向け解説】openCV顔検出の仕組と実践(detectMultiScale)](https://qiita.com/FukuharaYohei/items/ec6dce7cc5ea21a51a82)

### DlibDotNet の場合

DlibDotNet では、`FrontalFaceDetector`を作成して検出させます。

```csharp
    FrontalFaceDetector detector = Dlib.GetFrontalFaceDetector();    // ForntalFaceDetector作成  
```

そして、作成した`ForntalFaceDetector`で顔を検出します。

```csharp
    Rectangle[] rectangles  = detector.Operator(image);       // これで検出
    Rectangle[] rectangles2 = detector.Operator(image, 0);    // こんな感じで設定もできる
```

引数には、画像が入った`Array2D<RgbPixel>`を、第二引数を与えてパラメーターを調整できます。
返り値は、顔が入っていると思われる長方形の **配列** 。`Rectangle`の中身は以下の通り。

|プロパティ|説明|
|---:|---:|
|Left|長方形の左側のx座標。<br>元画像の左上が0で、右向き正。|
|Right|長方形の右側のx座標。<br>元画像の左上が0で、右向き正。|
|Top|長方形の上側のy座標。<br>元画像の左上が0で、下向き正。|
|Bottom|長方形の下側のy座標。<br>元画像の左上が0で、下向き正。|
|Width|長方形の横幅。|
|Height|長方形の縦幅。|

実際に使うときは OpenCvSharp3 のほうと同じように配列の中身があったり無かったり、複数あったりするので気を付けてください。

## FaceRecognitionDotNetをつかう
　DlibDotNetの作者が作ったもので、顔・特徴点の検出、顔の照合ができます。Nugetで配布中。
[FaceRecognition.Net](https://github.com/takuya-takeuchi/FaceRecognitionDotNet)

# 2D上の点を3D上の点に

2D上の物を3D上の物に変換する問題をPnP問題と言い、それを解くための関数が OpenCvSharp3 にあります。・・・しかし、この問題（2D→3D、位置・回転特定)を解くには最低でも6点の3D上の位置関係がわかっている点が必要です。そのために顔の特徴的な点を見つけます。なお、以下の記事がよくまとまっていて、どういう問題か把握するのに役にたちました。（問題を解くところは置いといて）
※参考 : [カメラの位置・姿勢推定２　PNP問題　理論編](http://daily-tech.hatenablog.com/entry/2018/01/21/185633)

## 顔の特徴点を特定

それでは顔の特徴点を見つけていきます。そのために、`ShapePredictor`を作成します。
Face Landmarkのファイルは公式が配布しているのでそれを使いましょう（クリックするとダウンロードされます。）
[shape pridictor 5 face landmarks](http://dlib.net/files/shape_predictor_5_face_landmarks.dat.bz2)
[shape pridictor 68 face landmarks](http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2)

```csharp
    ShapePredictor shape = ShapePredictor.Deserialize("FACE_LANDMARK_FILE_NAME.dat");    // ShapePredictor 作成
```

作成した`ShapePreidictor`を使って顔の特徴点を抽出します。

```csharp 
    // 5 landmarks だったらここも5
    DlibDotNet.Point[] points = new DlibDotNet.Point[68];
    //特徴点抽出
    using (FullObjectDetection shapes = shape.Detect(image, rectangles))
    {    // 5 landmarks だったらここも5
         for (uint i = 0; i < 68; i++)
         {  // DlibDotNet.Point という扱いやすい形に変える。
            points[i] = shapes.GetPart(i);
         }
    }
```

ここで注意したいのは、<font color="red">ただの`Point`だと、UnityのものとDlibDotNetのものがあり、エラーが出る</font>ということです。 `DlibDotNet.Point` としましょう。
Detect( , )の（第一引数, 第二引数）は、（入力画像, 顔がある領域）です。返り値は、検出点をまとめたオブジェクト `FuLLObjectDetection` です。
GetPart( )の（引数）は、（ほしい特徴点のインデックス）です。返り値は、指定されたインデックスの点 `DlibDotNet.Point` です。

上記のものがどのように検出されるかとか、他の使い方とかは以下の記事に詳しくのっているので、そちらを見てください。
※参考 : [Facial landmarks with dlib](https://www.pyimagesearch.com/2017/04/03/facial-landmarks-dlib-opencv-python/)
※参考 : [機械学習のライブラリ dlib](https://qiita.com/nonbiri15/items/9561c8194ba0b2041bd0)

### DlibDotNet を改造

~~私の環境では `ShapePredictor.Deserialize("")` がうまく動作しなかった（Dlibのdllにはなぜか渡すはずの文字列にランダムで何か追加された）ので、DlibDotNetに修正を入れてビルドしました。該当箇所は以下です。~~
修正が入ました。2019.1202以降は問題ありません。ここは読み飛ばしてください。

```csharp:src/DlibDotNet/ImageProcessing/ShapePredictor.cs
60 -    var str = Dlib.Encoding.GetBytes(path);                      //削除
61 -    var ret = NativeMethods.deserialize_shape_predictor(str,     //削除

60 +    var ret = NativeMethods.deserialize_shape_predictor(path,    //追加
```

```csharp:src/DlibDotNet/PInvoke/ImageProcessing/ShapePredictor.cs
26 +    [DllImport(NativeLibrary, CallingConvention = CallingConvention)]    //追加
27 +    public static extern ErrorType deserialize_shape_predictor(string filName, out IntPtr predictor, out IntPtr errorMesage);    //追加
28 +    //追加
```

## PnP問題を解く

PnP問題を解くのですが、私の場合、なぜか関数に`Mat`を引数として渡してやる方法しか成功しなかったのでそれを紹介します。
まず、現実の物体の3次元の座標をセットします。私の場合、両目の内側・外側・鼻先・口の両端・あごをセットしました。

```csharp
    Point3f[] model_points = new Point3f[8];                    // Matにセットする用の配列
    model_points[0] = new Point3f(0.0f, 0.03f, 0.11f);          // 鼻先
    model_points[1] = new Point3f(0.0f, -0.06f, 0.08f);         // あご
    model_points[2] = new Point3f(-0.048f, 0.07f, 0.066f);      // 右目外側
    model_points[3] = new Point3f(0.048f, 0.07f, 0.066f);       // 左目外側
    model_points[4] = new Point3f(-0.03f, -0.007f, 0.088f);     // 右くちびる
    model_points[5] = new Point3f(0.03f, -0.007f, 0.088f);      // 左くちびる
    model_points[6] = new Point3f(-0.015f, 0.07f, 0.08f);       // 右目内側
    model_points[7] = new Point3f(0.015f, 0.07f, 0.08f);        // 左目内側

    model_points_mat = new Mat(model_points.Length, 1, MatType.CV_32FC3, model_points);    // Matに変換  
```

そして、画像から検出された特徴点の使うやつをまとめます。

```csharp
    Point2f[] image_points = new Point2f[8];                      // Matにセットする用の配列
    image_points[0] = new Point2f(points[30].X, points[30].Y);    // 鼻先
    image_points[1] = new Point2f(points[8].X, points[8].Y);      // あご
    image_points[2] = new Point2f(points[45].X, points[45].Y);    // 右目外側
    image_points[3] = new Point2f(points[36].X, points[36].Y);    // 左目外側
    image_points[4] = new Point2f(points[54].X, points[54].Y);    // 右くちびる
    image_points[5] = new Point2f(points[48].X, points[48].Y);    // 左くちびる
    image_points[6] = new Point2f(points[42].X, points[42].Y);    // 右目内側
    image_points[7] = new Point2f(points[39].X, points[39].Y);    // 左目内側

    Mat image_points_mat = new Mat(image_points.Length, 1, MatType.CV_32FC2, image_points);    // Matに変換
```

さらに、PnP問題を解く関数を使うための設定を作っていきます。カメラのゆがみを設定するそうですが、よくわからなかったので理想モデルを元にしています。

```csharp
    Mat dist_coeffs_mat = new Mat(4, 1, MatType.CV_64FC1, 0);
    int focal_length = image.Cols;
    Point2d center = new Point2d(image.Cols / 2, image.Rows / 2);
    double[,] camera_matrix = new double[3, 3] { { focal_length, 0, center.X }, { 0, focal_length, center.Y }, { 0, 0, 1 } };
    Mat camera_matrix_mat = new Mat(3, 3, MatType.CV_64FC1, camera_matrix);
```

結果受け取り用の変数を作り、関数を適用させれば完成...

```csharp
    Mat rvec_mat = new Mat();    // 回転ベクトル
    Mat tvec_mat = new Mat();    // 位置ベクトル
    Cv2.SolvePnP(model_points_mat, image_points_mat, camera_matrix_mat, dist_coeffs_mat, rvec_mat, tvec_mat);    // 関数適応!  
```

と書きたいところですが、このままじゃ使えないのでUnityで扱える形にかえてやります。

```csharp
    Mat projMatrix_mat = new Mat();                   // 投影ベクトル
    double[] pos_double = double[3];                  // 位置の受け取り用
    double[] proj = double[9];                        // 回転行列受け取り用

    Marshal.Copy(tvec_mat.Data, pos_double, 0, 3);    // 位置の受け渡し
    Cv2.Rodrigues(rvec_mat, projMatrix_mat);          // 回転行列化
    Marshal.Copy(projMatrix_mat.Data, proj, 0, 9);    // 回転行列受け渡し

    Vector3 obj_position = default;                   // 最終的な位置
    Vector3 obj_rotation = default;                   // 最終的な回転

    obj_position.x = -(float)pos_double[0];           // 座標軸が違うのでこのよう
    obj_position.y = (float)pos_double[1];
    obj_position.z = (float)pos_double[2];
    obj_rotation = RotMatToQuatanion(proj).eulerAngles;  // 回転行列からクォータニオン、クォータニオンからオイラー角に変換
    // なお、RotMatToQuatanion()は自作関数で、下に畳んであります。
```

<details><summary>`Quaternion RotMatToQuatanion(double[] projmat)` </summary><div>

```csharp:RotMatToQuatanion.cs
Quaternion RotMatToQuatanion(double[] projmat)
    {
        Quaternion quaternion = new Quaternion();
        double[] elem = new double[4]; // 0:x, 1:y, 2:z, 3:w
        elem[0] = projmat[0] - projmat[4] - projmat[8] + 1.0f;
        elem[1] = -projmat[0] + projmat[4] - projmat[8] + 1.0f;
        elem[2] = -projmat[0] - projmat[4] + projmat[8] + 1.0f;
        elem[3] = projmat[0] + projmat[4] + projmat[8] + 1.0f;

        uint biggestIndex = 0;
        for (uint i = 1; i < 4; i++)
        {
            if (elem[i] > elem[biggestIndex])
            {
                biggestIndex = i;
            }
        }

        if (elem[biggestIndex] < 0.0f)
        {
            return quaternion;
        }

        float v = (float)Math.Sqrt(elem[biggestIndex]) * 0.5f;
        float mult = 0.25f / v;

        switch (biggestIndex)
        {
            case 0:
                quaternion.x = v;
                quaternion.y = (float)(projmat[1] + projmat[3]) * mult;
                quaternion.z = (float)(projmat[6] + projmat[2]) * mult;
                quaternion.w = (float)(projmat[5] - projmat[7]) * mult;
                break;
            case 1:
                quaternion.x = (float)(projmat[1] + projmat[3]) * mult;
                quaternion.y = v;
                quaternion.z = (float)(projmat[5] + projmat[7]) * mult;
                quaternion.w = (float)(projmat[6] - projmat[2]) * mult;
                break;
            case 2:
                quaternion.x = (float)(projmat[6] + projmat[2]) * mult;
                quaternion.y = (float)(projmat[5] + projmat[7]) * mult;
                quaternion.z = v;
                quaternion.w = (float)(projmat[1] - projmat[3]) * mult;
                break;
            case 3:
                quaternion.x = (float)(projmat[5] - projmat[7]) * mult;
                quaternion.y = (float)(projmat[6] - projmat[2]) * mult;
                quaternion.z = (float)(projmat[1] - projmat[3]) * mult;
                quaternion.w = v;
                break;
        }

        return quaternion;
    }
```

</div>


なお、ここの内容はほぼここを元にしました。
※参考 : [OpenCVで顔向き推定を行う](https://qiita.com/TaroYamada/items/e3f3d0ea4ecc0a832fac)
※参考 : [Head Pose Estimation using OpenCV and Dlib](https://www.learnopencv.com/head-pose-estimation-using-opencv-and-dlib/)

# 画像を表示したい

デバッグ用とかで、カメラ画像、もしくは加工した画像を見たいと思うことがあると思います。たとえば、正確に特徴点がとれているか調べるとか。
そこで、`Cv2.Imshow(string name, Mat image)`という関数があるのですが、<font color="red">罠です</font>。

<font color="red">もう一度書きます。**Cv2.Imshow(string name, Mat image)は罠です。**</font>

もしそのプログラムを走らせたらフリーズして、再起動しないといけません。
ではどうするかというと、 `Mat`を`Texture2D`に変換して、それを Plane などのオブジェクトに反映させてやります。

# おわりに
ここまでとても長かったと思いますが、読んでくださりありがとうございます。何か助けになれたのなら幸いです。