Help us understand the problem. What is going on with this article?

OpenSiv3D+dlibで顔器官検出する

概要

顔をトラッキングして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を作ります。

Face.hpp
#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として持っておきます。

Face.cpp
#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()でカメラを回し始めて
別スレッドでずーーーーっと顔の検出をしてる感じで

FaceDetector.hpp
# 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が落ちちゃってカクカクになります。

FaceDetector.cpp
#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());
}

まぁこんな感じで。

動かしてみる

顔の点を取って番号でも表示してみようかと思います。

Main.cpp
# 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);
        }
    }
}

スクリーンショット.png

動いた!

終わりに

とりあえず動くところまでは解説できました。
dlibをC++で使っているドキュメントが思っていたより少なくて結構大変でした。
しかもlibをビルドしたあとにソースファイルをインクルードし始めたりして訳が分からん
ちなみにpythonならpip installするだけなので一行で環境が整います。
dlibについてもっとちゃんと勉強したい。

質問とか改善案があればコメントに投げるか僕に連絡ください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away