- この記事はOpenCV Advent Calendar 2021の1日目の記事です。
- 他の記事は目次にまとめられています。
■ 顔向き推定をOpenCVの機能だけで実現したい!
OpenCVの"CV"とは、Comuter Visionのことである。
近年のOpenCVは、Vision以外にもAudioにも手を出していますが……
Commuter Visionにもいろいろな種類の課題がある。
- 対象:人物, 動物(猫, 犬, ...), 機械(車, 飛行機, ...
- 部位:顔 / 目 / 全身 / 上半身 / 下半身
- 目的:検出, 認証, 状態推定, ...
- 手段:機械学習(SVM, DNN, ...)
さて、今回は「顔向き推定の実現」というテーマで、OpenCVのみで行うチュートリアルを作っていきたい。
◯おいおい、顔向き推定なんて皆やってるだろ?と思われるかもしれませんが...
顔向き推定について書かれているドキュメントは、確かに少なくないです。
- Head Pose Estimation using OpenCV and Dlib
- PythonとOpenCV+dlibを用いた頭部方向推定
- webカメラとOpenCV.jsとブラウザでリアルタイム顔向き推定を行う
ただ、すぐ使えるかというと、色々難しい。
- 顔向き推定をOpenCV単独で完結できているサンプルが見つからない(DilibやTensorflowなど外部ライブラリ併用が前提)
- 学習済みデータ、モデルデータの入手について省略されている場合もある。
そこで今回は「OpenCV単体だけで完結する、顔向き推定」をまとめたいです。
■ 開発環境
今回は、Ubuntu 21.10上で、OpenCV 4.5.4をベースに検討します。Windows / MacOS環境の方は気合と勘で何とかしてください。
◯初めに
本テストを行うには、opencvだけでなく、opencv-contribも必要になります。
work
├ build (下記cmakeコマンドで作成)
├ opencv-4.5.4/modules
└ opencv_contrib/modules
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]フォルダのの下にあるので、要注意。
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のソースコードを入れたフォルダにダウンロードされているかもしれない。
./opencv-4.5.4/.cache/data/7505c44ca4eb54b4ab1e4777cb96ac05-face_landmark_model.dat
このファイルは、3条BSDでの配布に見えます。使うときはくれぐれもライセンスのドキュメントを熟読して使ってください。
■ 実行結果
(サンプルコードはこのページの下部に折りたたんであります。)
テストデータは、 "https://github.com/opencv/opencv_contrib/tree/4.x/modules/face/tutorials/face_landmark/images" を使う。
■ アルゴリズムの概略
- contrib/face moduleを使って、顔のlandmarkの2次元上での座標を入手する
- 3次元上で、各landmarkの座標を「仮に」決める。
- 2次元と3次元の座標変換するための、matrixを得る。
- 仮の3次元座標上で、法線となるベクトルを定義して、これを2次元に置き換える
この時、「鼻」「顎」「左目の左端」「右目の右端」「口の両端」が、landmarkの対象となっている。
◯現状でできること
とりあえず、あっち向いてホイ!はできますね……
◯もっと頑張れば...
ここから、更に行列計算をしっかりやれば、3次元空間上での顔の向き推定までできるはず…だけど力尽きたので、今回はここまで。
また、今回の顔のlandmark抽出を使うと、lip syncなんかもできそうな感じですね‥‥ ふむふむ。
■ まとめ:顔向き推定をOpenCVの機能だけで実現できる!
OpenCVのcontrib/face moduleを使う事で、顔向き推定がOpenCVのみで実現できることを確認できました!
サンプルコードを下記に置きました。
サンプルコード
■ 顔向き推定のプログラム
◯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
/*
* 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
// 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の推定
// 仮の3次元空間と、2次元空間上の座標を変換する行列を算出
// (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) );
}
// 今回使った2次元上の特徴点のプロット
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;
}