>   >  Unityでつくるインタラクティブコンテンツ:第4回:顔検出まとめ
第4回:顔検出まとめ

第4回:顔検出まとめ

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

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>顔検出アルゴリズムいろいろ

その他の連載