Unity3D
OpenCV
Unity
ユニティちゃん
dlib

iPhoneXがなくてもアニ文字したいんだ! 【Unity×OpenCV×Dlib】

自己紹介

こんにちは、Life is Tech !でUnityメンターをしているタナカです。(本名は岩﨑謙汰)

「思いついたらすぐ実装する」をモットーに2年間ほどUnityでの個人開発をしています(たまに受注的なこともします)。最近はHoloLensやARの開発をenjoyしています。

さて、最近面白いと思った動画がありまして、

笑ってはいけないアニ文字がツボに入ってしまった。

これ見てアニ文字って面白いなーと思ったんです。しかし、僕はiPhoneXを持っていない...iPhoneXが無くてもアニ文字をしたい!ということで、アニ文字ならぬ、ユニ文字 (ユニティちゃんの顔文字)をつくろうと思いました。

思い立ったら実装しましょう!(前日の出来事)

今回の制作物

1日での実装ということで、今回人の表情からユニティちゃんの表情を変化させるというところまでできました。

完成物

どうやったのか

今回の処理の流れとしては
① Webカメラで表情画像の取得
② OpenCVとDlibで眉、目、口の輪郭ポイントを取得
③ ユニティちゃんの表情を表現

となります。

image.png

機械学習周りは既存のライブラリで実現するとして、これらをUnity内で実装することを目標としました。Unityで実装することによって、iOS,Android,WebGL,PCと様々なプラットフォームにExportできます。

開発環境

  • Unity2017.1.0 (←5.3.8以上なら可)
  • MacBook Pro (Retina, 13-inch, Early 2015) プロセッサ 3.1 GHz Intel Core i7 メモリ 16 GB

このスペックでもメモリが足りなくなってUnityがよく落ちました (OpenCVの処理が重い)。

使用アセット

Dlib FaceLandmark Detector によって顔のモーションのキャプチャすることができます。OpenCVによってキャプチャの精度向上と顔の向きの検出ができます(顔の向きの検出は今回やっていない)[1,2]。
AssetStoreのUnitychanではなく、ユニティちゃん データ Ver.1.2を使用したのは、元から髪やスカートが揺れる設定がされているからです。ゼロから設定する場合はこちらの記事[3]を見てやろうと思っていました。

安く済ませたい && 実装力に自信がある人

OpenCV for Unityの値段高いよーという人はC++のOpenCV(無料)でネイティブプラグインを書くか、OpenCVSharp(無料)で頑張るのもありだと思います。

しかし、相当な実装力が必要となります。

参考記事 マルチプラットフォーム対応のOpenCV画像処理アプリをUnityで効率的に作る方法

制作過程

画像から輪郭ポイントの取得までの流れは、[1,2]の記事に紹介されているので多少割愛しながら説明しています。

OpenCV for Unityのインポート

こちらの記事を見ながら進めましょう。
UnityでOpenCVを利用した顔検出・画像処理アプリ事始め

“OpenCVForUnity/StreamingAssets/”フォルダを“Assets/”フォルダ直下に移動する

というのを忘れずにしましょう。

Dlib FaceLandmark Detectorのインポート

これもこちらの記事を見ながら進めましょう
UnityでDlibFaceLandmarkDetectorを利用した顔器官検出アプリ事始め

DlibFaceLandmarkDetectorWithOpenCVSample.unitypackageが内包されているので、こちらもimportしましょう。

Webカメラ画像から輪郭ポイントを作成する

空のオブジェクトでWebCameraSettingという名前のオブジェクトを作ります(名前はなんでもいいです)。そこに元々あるWebCamTextureToMatHelper.csをアタッチした後に、新しくWebCameraToFace.csを作成(こちらも名前はなんでもいい)し、アタッチします。

FaceRig無しでも中の人(二次元)になりたい!【Unity × OpenCV × Dlib × Live2D】

の記事にも書いてあるように、WebCameraToFace.csはDlibFaceLandmarkDetectorWithOpenCVSample内のWebCamTextureToMatSampleをベースにして書きました。

WebCameraToFace.cs(空オブジェクトにアタッチ)
[RequireComponent (typeof(WebCamTextureToMatHelper))]
    public class WebCameraToFace : MonoBehaviour
    {
        [SerializeField] FaceController faceController; //インスペクターから代入

        (中略)
 // Update is called once per frame
        void Update ()
        {
            if (webCamTextureToMatHelper.IsPlaying () && webCamTextureToMatHelper.DidUpdateThisFrame ()) {

                Mat rgbaMat = webCamTextureToMatHelper.GetMat ();

                OpenCVForUnityUtils.SetImage (faceLandmarkDetector, rgbaMat);

                //detect face rects
                List<UnityEngine.Rect> detectResult = faceLandmarkDetector.Detect ();

                foreach (var rect in detectResult) {

                    //detect landmark points
                    List<Vector2> points = faceLandmarkDetector.DetectLandmark (rect);
                    //draw face rect
                    OpenCVForUnityUtils.DrawFaceRect (rgbaMat, rect, new Scalar (255, 0, 0, 255), 2);

                    if (points.Count > 0) {
                        faceController.FaceModelUpdate(points);
                        break;
                    }
                }

               (中略)
            }
        }

輪郭ポイントをユニティちゃんの表情データに変換する

新しくFaceController.csを作成します。ユニティちゃんにアタッチします。

FaceController.cs(ユニティちゃんにアタッチ)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class FaceController : MonoBehaviour
{
    FaceView faceView;
    // Use this for initialization
    void Awake ()
    {
        faceView = GetComponent<FaceView>();
    }
    // 引数は輪郭ポイントのリスト
    public void FaceModelUpdate (List<Vector2> points)
    {
        float eyeOpen_L = getRaitoOfEyeOpen_L (points);
        float browOpen_L = getRaitoOfBROW_L_Y (points);
        float mouthOpen = getRaitoOfMouthOpen_Y(points);
        float mouthSize = getRaitoOfMouthSize(points);

        if (eyeOpen_L >= 0.85f) {
            faceView.SetEyeState (EyeState.Open);
        } else if (eyeOpen_L >= 0.75f) {
            faceView.SetEyeState (EyeState.Half);
        } else {
            faceView.SetEyeState (EyeState.Close);
        }

        if (browOpen_L >= 0.7f) {
            faceView.SetBrowState(BrowState.High);
        } else if (browOpen_L < 0.7f && browOpen_L >= -0.7f) {
            faceView.SetBrowState(BrowState.Middle);
        } else {
            faceView.SetBrowState(BrowState.Low);
        }
        if(mouthOpen < 0.2f) mouthOpen = 0;
        if(mouthSize < 0f) mouthSize = -1;
        faceView.SetMouthVal(mouthOpen,mouthSize);
    }
    ・・・(省略)

省略部分は

FaceRig無しでも中の人(二次元)になりたい!【Unity × OpenCV × Dlib × Live2D】

の記事のコードの

private float getRaitoOfEyeOpen_L (List<Vector2> points){}
private float getRaitoOfBROW_L_Y (List<Vector2> points){}
private float getRaitoOfMouthOpen_Y(List<Vector2> points){}
private float getRaitoOfMouthSize(List<Vector2> points){}

の部分を参考に書きました。

FaceModelUpdate関数では、口の幅や目の大きさを抽出し、表情のパラメータを決定しています。ここは今後とも微調整が必要です。

ユニティちゃんの表情の変え方

ユニティちゃんの表情の変え方は実は2種類あって、

1. 元々準備されたAnimationで表情を変える方法
2. ブレンドシェイプのパラメータを変化させて表情を変える方法

があります。

今回はパラメータによって自由に変更させたいので2番目の方法で行います。

ブレンドシェイプの設定はここでできます。

image.png

変えるとこのように変化します。

配列のindexによって、どの表情に寄せていくか変わります。基本は一つのindexの値に対して0~100の間に値を変化させます。僕はEYE_DEFとEL_DEFはindex=6(上から7番目)、BLW_DEFはindex=0(上から1番目)、MTH_DEFはindex=0と5(1番目と6番目)を変化させました。

(注 この値はキャラクターモデルによって変わります。自分で確認して微調整しましょう)

さぁ、これをScriptから変更できるようにコードを書きましょう。

目と眉毛は3段階に分ける

目は全開、半開、閉じるの3状態を持つようにしました。眉毛も同様に3状態です。

public enum EyeState{
    Open,
    Half,
    Close
}
public enum BrowState{
    High,
    Middle,
    Low
}

シームレスに動かす

3状態に分けると、目の動きが瞬間移動してしまうので、連続して動くように設定します。

FaceView.cs(ユニティちゃんにアタッチする)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FaceView : MonoBehaviour {

    [SerializeField]SkinnedMeshRenderer MTH_DEF; //MTH_DEFをInspectorから代入する
    [SerializeField]SkinnedMeshRenderer BLW_DEF;
    [SerializeField]SkinnedMeshRenderer EYE_DEF;
    [SerializeField]SkinnedMeshRenderer EYE_DEF2;

    [Range(0,100)]
    public float EyeParam = 0;
    float eyeParam=0;

    [Range(0,100)]
    public float BrowParam = 0;
    float browParam = 0;

    [Range(0,100)]
    public float MouthParam = 0;

    int eyeIndex = 6;
    int browIndex = 0;
    public bool IsDebug = true;

    public float speed = 2;
    // Use this for initialization
    void Start () {
        eyeIndex = 6;
        browIndex = 0;
    }

    // Update is called once per frame
    void Update ()
    {
        if (IsDebug) {
            EYE_DEF.SetBlendShapeWeight(eyeIndex,EyeParam);
            EYE_DEF2.SetBlendShapeWeight(eyeIndex,EyeParam);
            BLW_DEF.SetBlendShapeWeight(browIndex,BrowParam);
        } else {
            // シームレスに動かす
            EYE_DEF.SetBlendShapeWeight (eyeIndex, Mathf.Lerp (EYE_DEF.GetBlendShapeWeight (eyeIndex), eyeParam, Time.deltaTime*speed*5));
            EYE_DEF2.SetBlendShapeWeight (eyeIndex, Mathf.Lerp (EYE_DEF2.GetBlendShapeWeight (eyeIndex), eyeParam, Time.deltaTime*speed*5));

            BLW_DEF.SetBlendShapeWeight(browIndex,Mathf.Lerp (BLW_DEF.GetBlendShapeWeight (browIndex), browParam, Time.deltaTime*speed ));
        }

    }
    // FaceControllerから呼ぶ
    public void SetEyeState (EyeState state)
    {
        if (state == EyeState.Open) {
            eyeParam = 0;
        }else if(state == EyeState.Half){
            eyeParam = 50;
        }else{
            eyeParam = 100;
        }
    }
    // FaceControllerから呼ぶ
    public void SetBrowState (BrowState state)
    {
        if (state == BrowState.High) {
            browParam = 100;
        }else if(state == BrowState.Middle){
            browParam = 50;
        }else{
            browParam = 0;
        }
    }
    // FaceControllerから呼ぶ
    // open = [-1,1] size = [0,1]
    public void SetMouthVal (float open, float size)
    {
        MTH_DEF.SetBlendShapeWeight (0, open * 100);
        size = size / 2.0f + 0.5f;

        if (open > 0.3f) {
            MTH_DEF.SetBlendShapeWeight (5, size * 50);
        } else {
            MTH_DEF.SetBlendShapeWeight (5, 0);
        }

    }
}

参考記事 【Unity】Unity-Chanは二度笑う~ユニティちゃん表情モーション研究~その2

ここまでで一通り完成します。

苦労した点

実際にこのコードを実行してFPSを見てみると...

image.png

1秒間に3.6フレーム...これはやばい、最低でも30FPSは欲しいぞ

Profilerでみてみると...?

image.png

1フレームごとに顔検出させた結果、めちゃくちゃ重くなってる...ということなので、nフレーム置きに顔検出すると30FPSになりましたとさ (あまり間隔を空けすぎると、表情の検出が遅れるので注意)

他にいい方法があれば教えてほしいです!

おわりに

アニ文字のためには、顔の向きの反映と表情を豊かにする必要があるなーと思いました。まだまだ製作途中なのでがんばります。

顔の向きはOpenCVからわかるっぽいです。表情はモデルとにらめっこしていく必要になります。

今回PCのUnity上での動作になってますが、もちろんiPhoneにもビルドすることができます。

この記事は見なかったことに
iPhoneX以外のiPhoneやAndroidでもアニ文字が使えるアプリ「SUPERMOJI -the Emoji App」が話題に!

あと書き終わって気づいたのですが、ARkit使えば実現できたかもですね。しかしOSに依存しない(Webカメラでできる)方がunityのクロスプラットファーム性を活かしてると思うので、これはこれでいいと思いました。

次回はAndroidメンターのあっすーです!どんな記事を書いてくれるか楽しみです!

参考記事

[1] FaceRig無しでも中の人(二次元)になりたい!【Unity × OpenCV × Dlib × Live2D】
[2] dlibで顔認識して、leap motionで手の動きとって、unityの人体モデルを動かしたよ!
[3] PhysicalBoneで髪の毛とかスカートとか揺らしてみる

ユニティちゃんライセンス

image.png

備考

この記事はLife is Tech ! メンター Advent Calendar 2017の15日目の記事です。