#概要
顔をトラッキングしてLive2DっぽいことができるソフトをOpenSiv3Dで作っています。
たまーーにダウンロードされて新人Vtuberが生まれます。
https://youtu.be/tcCwrTjiSQA
今回は、その顔器官検出の部分をゆる~~く解説します。
AdventCalender僕の当番の日まであと30分!書ききれるかな!
#準備
##OpenCVのダウンロード
https://opencv.org/releases/
からWindow用のOpenCVをダウンロードして展開します。
##dlibのダウンロード
http://dlib.net/
からdlibの最新版をダウンロードしてきて展開します。
##環境変数にパスを追加
コントロールパネル→システムとセキュリティ→システム→システムの詳細設定から環境変数を設定します。
環境変数 | 参照する場所 | 例 |
---|---|---|
Dlib_ALL | source.cppがあるフォルダ | C:\dlib-19.18\dlib\all |
Dlib_INCLUDE | dlibのフォルダ | C:\dlib-19.18 |
OpenCV_INCLUDE | OpenCVのincludeフォルダ | C:\OpenCV\opencv\build\include |
#プロジェクトを作る
##OpenSiv3DのProjectを作る
みんな大好きOpenSiv3DのProjectを作りましょう。
##プロジェクトにパスを通す
プロジェクト→[プロジェクト名]のプロパティ→C/C++→全般→追加のインクルードディレクトリに
$(OpenCV_INCLUDE)
$(Dlib_INCLUDE)
$(Dlib_ALL)
を追加します
##source.cppをSourceFileに追加
dlib-19.18/dlib/allの中にあるsource.cppをプロジェクトのSourceFileに追加します。
たぶんdlib動かすのに必須な奴らを全部まとめた奴です。
こいつでincludeするファイルのためにDlib_ALLの環境変数を用意してみました。
##shape_predictor_68_face_landmarksをダウンロードしておく
dlibで顔器官検出をするための学習済みデータが配布されているのでそれをダウンロードしておきます。
ダウンロードしたらApp以下にAssetsフォルダを作ってそこに入れておきます。
#実装
##Face
dlibで検出した顔の点を表現するclassを作ります。
#pragma once
#include <Siv3D.hpp>
class Face
{
private:
Array<Vec2> m_facePoints;
public:
Face() = default;
Face(const Array<Vec2>& facePoints);
void draw(Color drawcolor) const;
Array<Vec2> getFacePoints() const;
};
shape_predictor_68_face_landmarksでは68個のパーツのカメラ画像での座標が取れるのでそれをVec2のArrayとして持っておきます。
#include "Face.hpp"
Face::Face(const Array<Vec2>& facePoints)
{
m_facePoints = facePoints;
}
void Face::draw(Color drawcolor) const
{
if (m_facePoints.size() != 68)
{
return;
}
//Ear to Ear
for (size_t i = 1; i < 16; ++i)
{
Line(m_facePoints[i], m_facePoints[i - 1]).draw(drawcolor);
}
//Nose
for (size_t i = 28; i < 30; ++i)
{
Line(m_facePoints[i], m_facePoints[i - 1]).draw(drawcolor);
}
//left eyebrow
for (size_t i = 18; i < 21; ++i)
{
Line(m_facePoints[i], m_facePoints[i - 1]).draw(drawcolor);
}
//Right eyebrow
for (size_t i = 23; i < 26; ++i)
{
Line(m_facePoints[i], m_facePoints[i - 1]).draw(drawcolor);
}
//Bottom part of the nose
for (size_t i = 31; i < 35; ++i)
{
Line(m_facePoints[i], m_facePoints[i - 1]).draw(drawcolor);
}
//Line from the nose to the bottom part above
Line(m_facePoints[30], m_facePoints[35]).draw(drawcolor);
//Left eye
for (size_t i = 37; i < 41; ++i)
{
Line(m_facePoints[i], m_facePoints[i - 1]).draw(drawcolor);
}
Line(m_facePoints[36], m_facePoints[41]).draw(drawcolor);
//Right eye
for (size_t i = 43; i < 47; ++i)
{
Line(m_facePoints[i], m_facePoints[i - 1]).draw(drawcolor);
}
Line(m_facePoints[42], m_facePoints[47]).draw(drawcolor);
//Lips outerpart
for (size_t i = 49; i < 59; ++i)
{
Line(m_facePoints[i], m_facePoints[i - 1]).draw(drawcolor);
}
Line(m_facePoints[48], m_facePoints[59]).draw(drawcolor);
//Lips inside part
for (size_t i = 61; i < 67; ++i)
{
Line(m_facePoints[i], m_facePoints[i - 1]).draw(drawcolor);
}
Line(m_facePoints[60], m_facePoints[67]).draw(drawcolor);
}
Array<Vec2> Face::getFacePoints() const
{
return m_facePoints;
}
まぁ、こんな感じでええやろか。
##FaceDetector
今日の本題。
顔の検出器を作ります。
方針としては、
コンストラクタでカメラのIndexを指定
start()でカメラを回し始めて
別スレッドでずーーーーっと顔の検出をしてる感じで
# pragma once
# include <Siv3D.hpp>
# include <dlib/opencv.h>
# include <opencv2/highgui/highgui.hpp>
# include <dlib/image_processing/frontal_face_detector.h>
# include <dlib/image_processing/render_face_detections.h>
# include <dlib/image_processing.h>
# include <dlib/gui_widgets.h>
# include "Face.hpp"
class FaceDetector
{
private:
std::thread m_thread;
std::atomic<bool> m_abort = false;
bool m_isActive = false;
mutable std::mutex m_mutex;
cv::Mat m_temp;
cv::VideoCapture m_cap;
dlib::shape_predictor m_pose_model;
dlib::frontal_face_detector m_detector;
Face m_detectedFace;
dlib::cv_image<dlib::bgr_pixel> m_cimg;
void stop();
void run();
void updateFaceData();
Vec2 ToVec2(const dlib::point& p);
public:
FaceDetector() = default;
FaceDetector(int32 cameraNum);
void start();
Face getFace() const;
};
dlibの顔検出は少々重いので、別スレッドでやらないとFPSが落ちちゃってカクカクになります。
#include "FaceDetector.hpp"
FaceDetector::FaceDetector(int32 cameraNum)
{
m_cap = cv::VideoCapture(cameraNum);
m_detector = dlib::get_frontal_face_detector();
m_detectedFace = Face();
dlib::deserialize("Assets/shape_predictor_68_face_landmarks.dat") >> m_pose_model;
start();
}
Face FaceDetector::getFace() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_detectedFace;
}
void FaceDetector::start()
{
stop();
m_isActive = true;
m_thread = std::thread(&FaceDetector::run, this);
}
void FaceDetector::stop()
{
if (!m_isActive)
{
return;
}
m_abort = true;
m_thread.join();
m_abort = false;
m_isActive = false;
}
void FaceDetector::run()
{
while (!m_abort)
{
if (!m_cap.read(m_temp))
{
return;
}
m_cimg = m_temp;
updateFaceData();
}
}
void FaceDetector::updateFaceData()
{
std::vector<dlib::rectangle> faces = m_detector(m_cimg);
Array<dlib::full_object_detection> shapes;
for (unsigned long i = 0; i < faces.size(); ++i)
{
shapes.push_back(m_pose_model(m_cimg, faces[i]));
}
if(shapes.size() == 0)
{
return;
}
size_t mostBigFaceIndex = 0;
double maxFaceSize = (ToVec2(shapes[0].part(0)) - ToVec2(shapes[0].part(16))).length();
for (size_t i = 1; i < shapes.size(); ++i)
{
const double faceSize = (ToVec2(shapes[i].part(0)) - ToVec2(shapes[i].part(16))).length();
if (faceSize > maxFaceSize)
{
mostBigFaceIndex = i;
maxFaceSize = faceSize;
}
}
Array<Vec2> faceParts;
for (auto k : step<unsigned long>(68))
{
faceParts.push_back(ToVec2(shapes[mostBigFaceIndex].part(k)));
}
std::lock_guard<std::mutex> lock(m_mutex);
m_detectedFace = Face(faceParts);
}
Vec2 FaceDetector::ToVec2(const dlib::point& p)
{
return Vec2(p.x(), p.y());
}
まぁこんな感じで。
##動かしてみる
顔の点を取って番号でも表示してみようかと思います。
# include <Siv3D.hpp> // OpenSiv3D v0.4.2
# include "FaceDetector.hpp"
void Main()
{
Scene::SetBackground(Palette::White);
Font font(20);
//引数にカメラのIndexを渡す
FaceDetector faceDetector(0);
faceDetector.start();
Array<Vec2> facePoints;
Face face;
while (System::Update())
{
Face face = faceDetector.getFace();
facePoints = face.getFacePoints();
face.draw(Palette::Black);
for (size_t i = 0; i < facePoints.size(); i++)
{
font(i).drawAt(facePoints[i], Palette::Red);
}
}
}
動いた!
#終わりに
とりあえず動くところまでは解説できました。
dlibをC++で使っているドキュメントが思っていたより少なくて結構大変でした。
しかもlibをビルドしたあとにソースファイルをインクルードし始めたりして訳が分からん
ちなみにpythonならpip installするだけなので一行で環境が整います。
dlibについてもっとちゃんと勉強したい。
質問とか改善案があればコメントに投げるか僕に連絡ください。