cv::face::Facemark を使って 顔の特徴点抽出をさせてみた(OpenCV)

# きっかけ
[OpenCVを使って長い動画を一部だけ保存する方法](https://qiita.com/satsukiya/items/9647e20c4e27b3d0362a)
[動画からキャプチャ画像を撮る方法(OpenCV)](https://qiita.com/satsukiya/items/c9dfdf237f5c60c90820)
[動画から顔認識させてみた(OpenCV:python版)](https://qiita.com/satsukiya/items/80072bd8f607ba65f6ea)
に続いて立候補動画からOpenCVを使って画像処理していきます。

また例のごとく[opencv_contrib](https://docs.opencv.org/4.1.2/d3/d81/tutorial_contrib_root.html)にある関数ライブラリを使って良さげなものがないか探したのですが、
`cv::face::Facemark` 顔の特徴点抽出
があったので実装してみます。

初登場したのがOpenCVがversion 3.0 になってからで4年くらい前っぽいです。

言語ですが、Pythonだと`opencv`のみでの実装はできないようで機械学習ライブラリ`dlib`と併用しないと実現できなさそうです。(詳細は[PythonでOpenCVの顔認識を試してみた](https://qiita.com/s-kajioka/items/b9207812fc968161f78b)を参照)

# 準備
[opencv doc : Face landmark detection in an image](https://docs.opencv.org/4.1.2/d2/d42/tutorial_face_landmark_detection_in_an_image.html)
をみたのですが、

```C++
CascadeClassifier face_cascade;
face_cascade.load(cascade_name);:question:
Mat img = imread(image);
Ptr<Facemark> facemark = createFacemarkKazemi();
facemark->loadModel(filename);
```
`Feature2d`と同じ要領で特徴点判別用のクラスをオブジェクト化してメソッドを呼び出すのかと思ったのですが、
`filename`:thinking::thought_balloon::question:
マジイミフでした。

それで少し調べると、、、
[OpenCV Forum: LBF Facemark Tutorial Issue] (https://answers.opencv.org/question/182326/lbf-facemark-tutorial-issue/) のサンプルコードにヒントが書かれていて、学習済みの`lbfmodel.yaml`が必要とのことです。それで、実行フォルダ配下にこのyamlファイルをすることとします。

```sh
> wget https://raw.githubusercontent.com/kurnianggoro/GSOC2017/master/data/lbfmodel.yaml
```

あとは、判別メソッド`facemark->fit(grayMat, faces, marks);` の引数`faces`ですが、顔認識したエリアを`cv::Rect`で渡す必要があるので、前処理として今まで通り[動画から顔認識させてみた(OpenCV:C++版)](https://qiita.com/satsukiya/items/872952e490dc2aa7bbe9) で使った、カスケード`cv::CascadeClassifier`での顔認識は必要になります。




また、makeするときはインクルードファイルは`opencv2/face.hpp`、ライブラリファイルは`opencv_face`を追加します。

下記の通りにmakefileを作成しました。

```makefile:GNUmakefile
CXX = clang++
CXXFLAGS =  -I/usr/local/Cellar/opencv/4.1.1_2/include/opencv4/ -I/usr/local/Cellar/boost/1.68.0_1/include/
LDFLAGS = -L/usr/local/Cellar/opencv/4.1.1_2/lib/ -L/usr/local/Cellar/boost/1.68.0_1/lib/
LDLIBS =  -lopencv_core -lopencv_highgui -lopencv_imgcodecs -lopencv_imgproc -lopencv_objdetect -lopencv_face -lopencv_videoio -lboost_timer
CXXVERSION = -std=c++11

landmark:landmark.cpp
	$(CXX) $< -o $@ $(CXXFLAGS) $(CXXVERSION) $(LDFLAGS) $(LDLIBS)

clean :
	rm landmark
```


# 開発 (画像のとき)

```c++
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/objdetect.hpp>
#include <opencv2/face.hpp>

int main( int argc, char** argv )
{

	cv::Mat imageMat = cv::imread("./capture_2.png", cv::IMREAD_COLOR );
	cv::Mat grayMat;

	cv::cvtColor(imageMat, grayMat, cv::COLOR_BGR2GRAY);

	// 顔全体のカスケード
	std::string cascadePath = "/usr/local/Cellar/opencv/4.1.1_2/share/opencv4/haarcascades/haarcascade_frontalface_alt_tree.xml";
	cv::CascadeClassifier cascade;

	if(!cascade.load(cascadePath))
    	return -1;

    // 顔特徴点抽出用の判別器
	cv::face::FacemarkLBF::Params params;
	params.n_landmarks = 68; 
	params.initShape_n = 10; 
	params.stages_n = 5; 
	params.tree_n = 6; 
	params.tree_depth = 5; 
	params.model_filename = "lbfmodel.yaml"; 

	cv::Ptr<cv::face::Facemark> facemark = cv::face::FacemarkLBF::create(params);
	facemark->loadModel(params.model_filename);

    //顔がどこのあるか判定
	std::vector<cv::Rect> faces;
	cascade.detectMultiScale(grayMat, faces);

    //顔の中にある特徴点を判定
	std::vector<std::vector<cv::Point2f>> marks;
	facemark->fit(grayMat, faces, marks);

	//描画 顔全体
	for (int i = 0; i < faces.size(); ++i) {
		cv::rectangle(imageMat, faces[i], cv::Scalar( 0, 0, 255 ), 2);
	}

	//描画 顔特徴点
	for (int j = 0; j < marks[0].size(); j++){
		std::cout << "(" << marks[0][j].x << "," << marks[0][j].y << ")" << std::endl;
		cv::circle(imageMat, cv::Point(marks[0][j].x, marks[0][j].y), 2, cv::Scalar(0,255,0), -1);
	}

	cv::namedWindow("face landmark", cv::WINDOW_AUTOSIZE);
	cv::imshow("face landmark", imageMat);

	cv::imwrite("output.png", imageMat);
	cv::waitKey(0);

	cv::destroyAllWindows();
}
```

# 結果 (画像のとき)

```terminal:output
(590.9,322.147)
(591.217,336.236)
(591.884,350.683)
(593.762,364.571)
(597.275,378.365)
(604.097,391.034)
(613.718,402.907)
(625.272,411.426)
(637.967,413.993)
(650.729,411.284)
(662.937,404.078)
(674.311,394.063)
(683.501,382.459)
(688.967,368.631)
(691.895,353.862)
(693.695,338.896)
(694.573,323.792)
(599.207,314.833)
(604.014,306.059)
(614.227,303.123)
(624.255,304.961)
(633.462,309.416)
(643.505,308.756)
(653.441,304.1)
(664.577,302.964)
(675.373,306.003)
(681.47,314.908)
(639.315,322.427)
(639.228,331.317)
(639.165,339.942)
(639.073,349.015)
(628.861,356.563)
(633.756,358.463)
(639.046,359.731)
(644.456,358.639)
(649.343,356.906)
(609.223,323.647)
(614.408,320.867)
(621.07,321.2)
(626.829,326.116)
(620.658,326.787)
(613.902,326.445)
(652.744,326.402)
(658.855,321.455)
(665.723,321.071)
(671.209,324.189)
(666.49,326.816)
(659.414,327.152)
(621.229,376.468)
(628.091,373.888)
(634.395,371.597)
(637.988,373.133)
(641.814,371.826)
(647.98,374.261)
(655.28,377.79)
(647.95,383.186)
(642.081,385.906)
(637.896,386.386)
(633.731,385.89)
(627.789,383.062)
(624.089,376.941)
(634.286,376.723)
(637.913,377.249)
(641.863,376.869)
(652.192,377.998)
(641.86,378.551)
(637.961,379.266)
(634.188,378.507)
```

![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/55dfb175-7a12-e311-4cba-39bfbf6815cf.png)

いい感じに特徴点が出力されました。

# 開発 (動画のとき)

```c++
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/objdetect.hpp>
#include <opencv2/face.hpp>
#include <boost/timer/timer.hpp>

int main( int argc, char** argv )
{
	// 入力動画用
	cv::VideoCapture cap("./one_minutes.mp4");

	int cap_width = (int) cap.get(cv::CAP_PROP_FRAME_WIDTH);
	int cap_height = (int) cap.get(cv::CAP_PROP_FRAME_HEIGHT);
	double fps = cap.get(cv::CAP_PROP_FPS);

	// 出力動画用
	cv::VideoWriter writer("face_landmark.mp4", cv::VideoWriter::fourcc('m','p','4','v'), fps, cv::Size(cap_width, cap_height));

	// 顔全体のカスケード
	std::string srcFolder = "/usr/local/Cellar/opencv/4.1.1_2/share/opencv4/haarcascades/";
	std::string faceCascadePath = "haarcascade_frontalface_alt_tree.xml";

	cv::CascadeClassifier face_cascade;

	if(!face_cascade.load(srcFolder + faceCascadePath))
		return -1;

    // 顔特徴点抽出用の判別器
	cv::face::FacemarkLBF::Params params;
	params.n_landmarks = 68; 
	params.initShape_n = 10; 
	params.stages_n = 5; 
	params.tree_n = 6; 
	params.tree_depth = 5; 
	params.model_filename = "lbfmodel.yaml"; 

	cv::Ptr<cv::face::Facemark> facemark = cv::face::FacemarkLBF::create(params);
	facemark->loadModel(params.model_filename);

	boost::timer::cpu_timer timer;

    while(cap.isOpened()){
    	cv::Mat frame;
		cv::Mat grayFrame;
		cap >> frame;  // キャプチャ
		// 様々な処理

		if (frame.empty()){
			break;
		}

		cv::cvtColor(frame, grayFrame, cv::COLOR_BGR2GRAY);

	    //顔がどこのあるか判定
		std::vector<cv::Rect> faces;
		face_cascade.detectMultiScale(grayFrame, faces);

	    //顔の中にある特徴点を判定
		std::vector<std::vector<cv::Point2f>> marks;
		facemark->fit(grayFrame, faces, marks);

		//描画 顔全体
		for (int i = 0; i < faces.size(); ++i) {
			cv::rectangle(frame, faces[i], cv::Scalar( 0, 0, 255 ), 2);

			//描画 顔特徴点
			for (int j = 0; j < marks[i].size(); j++){			
				cv::circle(frame, cv::Point(marks[i][j].x, marks[i][j].y), 2, cv::Scalar(0,255,0), -1);
			}
		}

		writer << frame;

		faces.clear();

		for (int i = 0; i < marks.size() ;i++){
			marks[i].clear();
		}

		grayFrame.release();
		frame.release();

    }

	std::string result = timer.format(3,"処理時間:%w秒"); // 結果文字列を取得する
    std::cout << result << std::endl;

    writer.release();
    cap.release();

}
```

# 結果 (動画のとき)
![landmark.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/7eb802d5-8133-778f-e7d9-fe79a71013d7.gif)

今の方法では、デフォルトのカスケードを使っているので向き・傾きに弱いです。何かしらで学習すればトラッキング精度が上がると思います。

# おわりに
顔の特徴点が取れるということは巷で話題のVTuberはこの技術を使っていそうです。
体の骨格も抽出できるので、3Dオブジェクトと同期すれば何かと面白いことができそうです。
てかiPhoneのアニ文字ってdevelopとかで取得できないんでしょうか。:robot:
[![笑ってはいけないアニ文字がツボに入ってしまった。](http://img.youtube.com/vi/28jAR_LDNJE/0.jpg)](https://www.youtube.com/watch?v=28jAR_LDNJE)
[![笑ってはいけないアニ文字が面白すぎるwwww](http://img.youtube.com/vi/4czna7tkWlA/0.jpg)](https://www.youtube.com/watch?v=4czna7tkWlA)

# 参考にしたリンク
- [cv::face::Facemark Class Reference](file:///Users/satoukazuo/workspace/opencv_doc/db/dd8/classcv_1_1face_1_1Facemark.html)
- [opencv doc : Face landmark detection in an image](https://docs.opencv.org/4.1.2/d2/d42/tutorial_face_landmark_detection_in_an_image.html)
- [PythonでOpenCVの顔認識を試してみた](https://qiita.com/s-kajioka/items/b9207812fc968161f78b)
- [OpenCV Forum: LBF Facemark Tutorial Issue] (https://answers.opencv.org/question/182326/lbf-facemark-tutorial-issue/)