vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Unity】【LeapMotion】【Windows】ジェスチャー検出 その1

はじめに

諸事情により、Leap Motionを購入しました。
まぁ大した理由もないのですが、前からモーションセンサーは購入したいと思っていたので。

Leap Motionはハードウェアは据え置きでソフトウェアの進化のみでトラッキングの精度が上がっていくという稀有な存在だと思います(あまり自由にお金を使えない勢としてはありがたいことこの上ない)。

今回は、Orion BetaUnity Core Assets ver.4.1.2を使ってトラッキングの開始・停止や手を右から左に(またはその逆)素早く動かすSwipe操作などを検出してみたことについて書き残すことにします。

https://github.com/masanori840816/LeapMotionCtrl

ラッキングの開始・停止

まずは手のトラッキングの開始・停止を検出するところから。

MotionCtrl.cs

using Leap;
using UnityEngine;
using System.Collections.Generic;
using System.Linq;

public class MotionCtrl : MonoBehaviour {
    public List < GameObject > motionCtrlObjectList;

    private List < IMotionCallback > motionCtrlList;
    private int motionCtrlCount;

    private Controller leapCtrl;
    private Frame leapFrame;
    private List < Hand > handList;

    private bool isTracking;
    private bool isGrabbed;
    private bool lastTrackingStatus;
    private bool lastGrabbedStatus;
    private Vector3 convertedValue = new Vector3(0, 0, 0);

    private Vector3 handPosition;
    public Vector3 HandPosition
    {
        get { return handPosition; }
    }
    private void Start (){
        // Trackingと手の状態変化を返す対象のClass.
        motionCtrlList = motionCtrlObjectList.Select(ctrlObject => ctrlObject.GetComponent < IMotionCallback > ()).ToList();
        motionCtrlCount = motionCtrlList.Count - 1;

        // App起動時にLeapMotionに接続.
        leapCtrl = new Controller();
        // StartConnectionの実行は必要?
        leapCtrl.StartConnection();

        isTracking = false;
        isGrabbed = false;

        lastTrackingStatus = false;
        lastGrabbedStatus = false;
    }
    
    private void Update () {
        leapFrame = leapCtrl.Frame();
        handList = leapFrame.Hands;

        isTracking = (handList.Count > 0);

        if(isTracking != lastTrackingStatus)
        {
            // Trackingの開始・停止をCallbackで通知.
            if (isTracking)
            {
                for (var i = motionCtrlCount; i >= 0; i--)
                {
                    motionCtrlList[i].OnTrackingStarted();
                }
            }
            else {
                for (var i = motionCtrlCount; i >= 0; i--)
                {
                    motionCtrlList[i].OnTrackingStopped();
                }
                // 手の位置をリセット.
                handPosition.x = 0f;
                handPosition.y = 0f;
                handPosition.z = 0f;
            }
            lastTrackingStatus = isTracking;
        }

        if (! isTracking)
        {
            // Tracking中でなければ手の位置などは取得しない.
            return;
        }
        // Tracking中なら手(Hand0)の平の座標値を取得する.
        handPosition = ConvertToUnityVector3(handList[0].PalmPosition);
~省略~
    }
    private void OnApplicationQuit()
    {
        // Appの終了時は切断する.
        leapCtrl.StopConnection();
        leapCtrl.Dispose();
    }
    private Vector3 ConvertToUnityVector3(Vector originalValue)
    {
        // Leap.VectorからUnityのVector3に変換する.
        convertedValue.x = originalValue.x;
        convertedValue.y = originalValue.y;
        convertedValue.z = originalValue.z;
        return convertedValue;
    }
}
  • ラッキングなどの情報を得るためには、Leap.Controllerを使います。
  • アプリを閉じる前に、Leap.Controller.StopConnection()を実行して切断する必要があり、実行しないとアプリを閉じたタイミングでフリーズしてしまうことも。
  • 上記ではトラッキング中であれば0番目の手の座標値を取得しています。

ラッキングの開始・停止

Leap.Controller.Frame().Handsで取得した手の数が1以上、かつ前フレームと状態が変わった場合にコールバック関数を呼び出す、ということにしています。
が、もうちょっと簡単にできないかなぁとは思っています。

値の変化を検出するのなら、UniRxが使えるのでは?という気もしているのですが...。

コールバック関数

上記のMotionCtrl.csの情報を利用するClassでは、以下のInterfaceを継承して、トラッキングの開始・停止などの通知が受け取れるようにします。

IMotionCallback.cs

public interface IMotionCallback{
    void OnTrackingStarted();
    void OnTrackingStopped();
    void OnHandGrabbed();
    void OnHandReleased();
}

Grab(手を握る)の検出

手を握ったかどうかの検出は、Leap.Controller.Frame().Hands.Fingersが持つ「IsExtended」の値を見ることで判断できます。

MotionCtrl.cs

~省略~
    private void Update(){
    ~省略~
        // 手を握っているか(誤検知を考慮して開いている指の本数が1以下ならTrue).
        isGrabbed = (handList[0].Fingers.Where(finger => finger.IsExtended).Count() <= 1);
        if(isGrabbed != lastGrabbedStatus)
        {
            if (isGrabbed)
            {
                for (var i = motionCtrlCount; i >= 0; i--)
                {
                    motionCtrlList[i].OnHandGrabbed();
                }
            }
            else
            {
                for (var i = motionCtrlCount; i >= 0; i--)
                {
                    motionCtrlList[i].OnHandReleased();
                }
            }
            lastGrabbedStatus = isGrabbed;
        }
    }
~省略~

Swipeの検出

手を右から左、または上から下(+それぞれ逆方向)に素早く動かす、Swipe操作を検出します。

実はLeapMotionのVersion2.Xの時は、標準でジェスチャー検出が可能だったのですが、今回使用するVersion4.XではDeprecatedになっており、また今後実装される見込みも薄そうです。

ということで、自分でなんとかしてみることにします。

考え方

ある一定の速度で、ある一定の方向に手が動いているかを調べます。
Update関数で、前のFrameで取得した手の座標値と今の座標値とを比較して、その差分を取得します。

○                   ○                    ○                   ○
L前Frameの手の位置との差分 」L前Frameの手の位置との差分 」L前Frameの手の位置との差分 」

で、この値の合計値が一定以上ならSwipeとして処理を実行する、と。
ただし、斜めに動かした場合などは判別しづらいので、縦横逆方向に大きく動いている場合は無視することとします。

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

public class MotionSwipeCtrl : MonoBehaviour, IMotionCallback
{
    public GameObject motionCtrlObject;

    private readonly int MovedLengthCount = 3;
    // 
    private readonly float TargetDirectionPerFrameMoveLength = 25.0f;
    private readonly float OppositeDirectionPerFrameMoveLength = 5.0f;
    private readonly Vector2 DefaultMoveLength = new Vector2(0.0f, 0.0f);

    private MotionCtrl motionCtrl;
    private bool isCtrlDisabled;
    // X, Y方向のSwipeのみを取るためVector2を使う.
    private List movedLengthList;
    private float movedTotalLengthX;
    private float movedTotalLengthY;

    private Vector3 lastHandPosition;
    private Vector2 newMovedLength;

    private float validMinMoveLength;
    private float invalidMaxOppositeLength;

    private bool isGestureExecuted;

    public void OnTrackingStarted()
    {
        lastHandPosition = motionCtrl.HandPosition;
        isCtrlDisabled = false;
        isGestureExecuted = false;
    }
    public void OnTrackingStopped()
    {
        isCtrlDisabled = true;
        Reset();
    }
    public void OnHandGrabbed()
    {
        isCtrlDisabled = true;
    }
    public void OnHandReleased()
    {
        isCtrlDisabled = false;
    }
    private void Start(){
        motionCtrl = motionCtrlObject.GetComponent();
        movedLengthList = new List();
        validMinMoveLength = 0f;
        invalidMaxOppositeLength = 0f;
        // 初期化.
        for (var i = MovedLengthCount; i >= 0; i--)
        {
            movedLengthList.Add(DefaultMoveLength);

            validMinMoveLength += TargetDirectionPerFrameMoveLength;
            invalidMaxOppositeLength += OppositeDirectionPerFrameMoveLength;
        }
        isCtrlDisabled = true;
        isGestureExecuted = false;
    }
    private void Update () {
        if (isCtrlDisabled)
        {
            return;
        }
        // 前Frameからの手の座標値の差分を算出.
        newMovedLength.x = motionCtrl.HandPosition.x - lastHandPosition.x;
        newMovedLength.y = motionCtrl.HandPosition.y - lastHandPosition.y;

        if (! isGestureExecuted)
        {
            // 前数Frame分の移動距離を算出.
            movedTotalLengthX = Math.Abs(movedLengthList.Select(movedLength => movedLength.x).Sum() + newMovedLength.x);
            movedTotalLengthY = Math.Abs(movedLengthList.Select(movedLength => movedLength.y).Sum() + newMovedLength.y);

            if (movedTotalLengthX >= movedTotalLengthY)
            {
                if (movedTotalLengthX >= validMinMoveLength
                    && movedTotalLengthY <= invalidMaxOppositeLength)
                {
                    
                    // TODO: 横方向にSwipe操作した場合の処理.
                    
                    isGestureExecuted = true;
                    StopDetectingGesture();
                }
            }
            else
            {
                if (movedTotalLengthY >= validMinMoveLength
                    && movedTotalLengthX <= invalidMaxOppositeLength)
                {
                    
                    // TODO: 縦方向にSwipe操作した場合の処理.
                    
                    isGestureExecuted = true;
                    StopDetectingGesture();
                }
            }
        }
        
        // 値を更新して今Frameの座標値の差分を保存する.
        for (var i = MovedLengthCount; i >= 1; i--)
        {
            movedLengthList[i] = movedLengthList[i - 1];
        }
        movedLengthList[0] = newMovedLength;
        // 今Frameの座標値を保存する.
        lastHandPosition = motionCtrl.HandPosition;
    }
    private void Reset()
    {
        for (var i = MovedLengthCount; i >= 0; i--)
        {
            movedLengthList[i] = DefaultMoveLength;
        }
        isGestureExecuted = false;
    }
    private IEnumerator StopDetectingGesture()
    {
        // Swipe実行後は一定時間Swipe操作を無効にする.
        yield return new WaitForSeconds(0.5f);
        Reset();
    }
}
  • 手の座標値の差分を取得するFrame数やその合計値などは使用状況などによって調整してください。
  • 手を素早く動かし続けていると、Swipe処理が何度も実行されてしまうため、フラグで一定時間Swipe処理が実行されないようにしています。

おわりに

とりあえずそれっぽく動くようにはなりましたが、特にSwipeのしきい値などはまだ調整が必要そうです。

また、処理の内容の割にコードが長すぎる気がするので、ここももう少し工夫したいところです。

次回ももう少しLeapMotionであれこれやってみた話になる…予定です。

参考

Leap Motion

Linq