記事の目次

    前回は、複数人の顔を検出してお面をかぶせるところまでコードをアレンジしてみました。今回は、さらにその精度を高めつつ、他の顔検出アルゴリズムについても紹介していきます。

    TEXT_高田稔則 / Toshinori Takata(Codelight
    EDIT_小村仁美 / Hitomi Komura(CGWORLD)

    <1>複数人の顔を検出する際のチラつきを抑える

    こんにちは、Codelight株式会社の高田です。前回から少し時間が経ってしまいました。最近「mui」というIoTデバイスに、Kickstarterで支援しました、外観に本物の木を使っており、タッチパネルが内蔵されている情報端末です。デベロッパー版はRaspberry Piを使っての開発が可能とのことで、到着が楽しみです。

    さて、前回はOpenCVの顔検出を使って複数の人の顔にそれぞれお面を被せるプログラムを作成しました。今回はその解説を行います。併せて「JIDO-RHYTHM」に似たコンテンツの作成を考えていましたが、細かな解説ではプログラム的な話が多くなってしまうため、基本的な考え方を述べるにとどめ、コンテンツに使える顔検出に関してまとめていこうと思います。

    まず最初に、前回作成したソースにミスがあったので更新しました。前回行なった修正の目的は複数人への対応でした。しかし、検出された顔が1つの場合は、検出された位置にそのままお面を表示させれば問題ありませんが、いくつも顔として誤検出される場合や、あるフレームは検出され、次のフレームでは検出されないということも起こります。このような状況では、単純にお面を被せるとチラついてしまい、コンテンツを楽しむことができなくなります。そのため検出された顔のどれにお面を被せるか決めていくことが必要です。

    処理のながれとしては以下のようになります。

    1:OpenCVで顔を検出します。検出結果にはノイズも含まれるのでお面をかぶせる候補として保持します(227行目から235行目の処理)。

    // 検出した顔位置は以降何回も使うのでリストとして保持しておく
    OpenCVForUnity.Rect[] rects = faces.toArray();
    List detectedPos = new List();
    foreach (var rect in rects)
    {
    var cx = rect.x + rect.width / 2f;
            var cy = rect.y + rect.height / 2f;
            detectedPos.Add(new Vector2(cx, -cy));
    }
    

    2:検出候補点をすべてトラッキングし、一定時間トラッキングできたら候補を顔と判断します(239行目から290行目の処理)。

    // すでに検出している顔位置を新しい検出位置で更新する
    // トラッキングされている顔が無い場合、ここでは何もしない
    List useIds = new List();
    for (int i = 0; i < _detectFaces.Length; i++)
    {
        // トラッキングされていない顔はスキップ
        if (_detectFaces[i].TrackingState == DetectFace.State.None)
            continue;
    
        // 新しい検出点から最も近いトラッキングされている点を探す
        // 一定(_searchDist)以上離れている点は無視
        var closeId = -1;
        var minDist = Mathf.Infinity;
        for (int n = 0; n < detectedPos.Count; n++)
        {
            if (useIds.Contains(n)) continue;
            var dist = (_detectFaces[i].Pos - detectedPos[n]).magnitude;
            if (minDist > dist && dist < _searchDist)
            {
                minDist = dist;
                closeId = n;
            }
        }
    
        var df = _detectFaces[i];
        if (closeId > -1)
        {
            useIds.Add(closeId);
    
            df.LostTime = 1;
    
            // ノイズの平滑化
            df.Pos = Vector2.Lerp(df.Pos, detectedPos[closeId], _smooth);
    
            // 新しい検出位置を一定時間以上検出し続けていれば検出できたとみなす
            if (_detectFaces[i].TrackingState == DetectFace.State.Detecting)
            {
                df.DetectTime -= Time.deltaTime;
                if (df.DetectTime < 0)
                {
                    _detectFaces[i].TrackingState = DetectFace.State.Tracking;
                }
            }
        }
        else
        {
            // 新しい検出位置が見つからなかったので、LostTimeを減らす。これが0になるとトラッキングしなくなる
            df.LostTime -= Time.deltaTime;
            if (df.LostTime < 0f)
            {
                df.TrackingState = DetectFace.State.None;
            }
        }
    }
    

    3:お面を被せた顔が一定時間トラッキングできなくなったら、検出できなくなったと判断してお面を消す(283行目から289行目の処理)。

    // 新しい検出位置が見つからなかったので、LostTimeを減らす。これが0になるとトラッキングしなくなる
    df.LostTime -= Time.deltaTime;
    if (df.LostTime < 0f)
    {
        df.TrackingState = DetectFace.State.None;
    }
    

    この処理を行うために「検出」、「トラッキング」、「何もなし」の3つの状態があるとして管理しています。図にすると以下のようになります。

    1:「検出」
    この点を「顔候補リスト(detectedPos)」に追加(青い点)します。


    顔と思われる点が検出された

    2:「トラッキング」
    次のフレームで検出された点(赤い点)の距離を調べ、検索範囲内なら青い点が移動したものとみなします。次のフレームで見つかった緑の点が同じように赤い点の範囲内なら赤い点が移動したとみなします。この状態を一定時間くり返していれば、それは安定して認識されている顔とみなして「検出された顔リスト(_detectedFace)」に追加します。


    検索範囲内で点が見つかれば移動先とみなし、移動していると考えることができる

    3:「何もなし」
    しかし、以下のように一定距離内に点がない状態が続くと、認識がなくなったとして_detetedFaceから除外されます。


    移動するべき点が見つからない状態。検出点がなくなったか、緑の点がノイズであった可能性

    この状態を加えてながれをムービーで作成しました。テロップ右肩の赤文字が各状態です。

    基本的にやっていることはシンプルなので、コードと状態のながれを追ってみてください。質問などあれば弊社のTwitter(@codelight)宛に連絡いただければできるだけ対応します。顔ハメのコンテンツをつくるときは、こんな感じで検出とトラッキングが必要だということが伝わればと思います。

    次ページ:
    <2>顔検出アルゴリズムいろいろ

    [[SplitPage]]

    <2>顔検出アルゴリズムいろいろ

    今回のサンプルでは、最も広く使われているOpenCVの顔検出アルゴリズムを利用しました。手軽に扱えるので他にもいろいろ試してみてください。最近では機械学習などを利用して、さらに高度な認識を行う手法がたくさん出てきています。「JIDO-RHYTHM」ではおそらく、iPhone XからサポートされたARKitを使っていると思います。ARKitでは、iOSのTrueDepthカメラを使った顔認証システムのデータをアプリケーションから使えるようになっています。

    Unity-ARKitプラグインはそれをUnityから使えるようにしたものです。顔の位置だけではなく、パーツごとに顔を認識することができ、自分の顔と同じ表情をCGキャラクターに反映させるように使うことができます。ARKitをUnityで使う方法についてはたくさんの記事が公開されていますので、そちらに譲ります。余談ですが、最近顔検出を使ったコンテンツの相談がいくつかあり、いろいろ調べていた中では、Visage Technologiesのライブラリ「visage|SDK」がかなり優秀でした。

    このライブラリは、「FaceRig」という顔の動きを高精度にCGアバターに反映させることのできるアプリケーションでも使われています。比較的値段の高いライブラリですが、WEBカメラだけで3Dメッシュを顔にぴったり合わせてくれます。Unityへのインテグレーションにも対応していて、いろいろ面白いコンテンツをつくることができそうです。

    次回はセンサなどの外部デバイスをUnityで使うことを考えていきたいと思います。インタラクティブコンテンツは外部入力をいかにセンシングして処理するかが重要で、センサなどのハードウェアの連携ができれば表現が広がります。

    例えば、これは壁にタッチすると反応するコンテンツによく使われる北陽電機の測域センサーです。

    以下のムービーはこのセンサをUnityで使えるようにして、球を置いたり外したりしている様子です。ロケータがセンサの中心を表していて、そこから障害物までを線で表示するようなスクリプトを作成しました。ここから球の位置を特定することが可能になります。

    このセンサは価格的に個人ではなかなか手に入れにくいものだと思いますが、実際の現場で使われているものの良い例として紹介したいと思います。



    Profile.

    高田稔則/Toshinori Takata(Codelight)
    Codelight株式会社 代表取締役・インタラクションエンジニア
    フリーランス、株式会社TBSテレビ等で映画CG制作、株式会社ソニー・コンピュータエンタテインメント(現 ソニー・インタラクティブエンタテインメント)でPS4のOSD開発などを経て2006年にCodelight株式会社を設立。インタラクティブコンテンツの制作を中核として、製造業向けのプロトタイプ開発なども行う
    www.codelight.co.jp