MRが楽しい

MRやVRについて学習したことを書き残す

MRTKの手の検出イベントをUniRxのオブザーバで処理する その2(手のジェスチャーを判定する)

本日は MRTK と UniRx の小ネタ枠です。
MRTKの手の検出イベントをUniRxのオブザーバで処理する方法を記事にします。
今回は前回記事のコードに処理を追加して、手のジェスチャーを取得してみます。
f:id:bluebirdofoz:20210417225920j:plain

前提条件

前回記事の続きです。
bluebirdofoz.hatenablog.com

Buffer

Buffer は複数のメッセージを1つのメッセージに変換する UniRx のオペレータです。
今回はこのオペレータを使って、手のジェスチャーの判定結果の丸め込みを行います。

サンプルコード

以下のようなサンプルコードを作成しました。
3つの手の検出イベントそれぞれのオブザーバを作成し、ハンドジョイントの情報から手のジェスチャーを判定します。
更に細かな状態の変動を防ぐため、複数フレームの判定結果から最も多く判定されたジェスチャーの結果で丸め込みを行っています。
・TestHandStatusBuffer.cs

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

using System;

using Microsoft.MixedReality.Toolkit;
using Microsoft.MixedReality.Toolkit.Input;
using Microsoft.MixedReality.Toolkit.Utilities;

using UniRx;

namespace HMProject.Test
{
    public class TestHandStatusBuffer : MonoBehaviour, IMixedRealityHandJointHandler, IMixedRealitySourceStateHandler
    {
        /// <summary>
        /// ハンドタイプ(左右)
        /// </summary>
        [Serializable]
        private enum HandType
        {
            Right,
            Left,
        }

        /// <summary>
        /// 手の状態識別
        /// </summary>
        [Serializable]
        public enum HandStatus
        {
            Normal,           // 通常
            Gur,              // グー
            Choki,            // チョキ
            Par,              // パー
            Pistol,           // ピストル
            ThumbsUp,         // サムズアップ
        }

        [SerializeField, Tooltip("ハンドタイプ(左右)の設定")]
        private HandType p_SettingHandType;


        [SerializeField, Tooltip("親指の折れ曲がり判定角")]
        private float p_HoldThumbAngle = 30.0f;

        [SerializeField, Tooltip("四指の折れ曲がり判定角")]
        private float p_HoldFingerAngle = 60.0f;

        [SerializeField, Tooltip("手の状態を丸め込み判定するバッファサイズ")]
        private int p_HandStatusBufferCount = 10;


        [SerializeField, Tooltip("現在の手の状態")]
        private HandStatus p_CurrentHandStatus;

        [SerializeField, Tooltip("現在の手のソースID")]
        private uint p_CurrentHandSourceId;


        // 丸め込み用カウンタリスト
        private int[] p_RoundingCountList;


        /// <summary>
        /// 起動処理
        /// </summary>
        void Start()
        {
            // 丸め込み用カウンタリストを作成しておく
            p_RoundingCountList = new int[Enum.GetNames(typeof(HandStatus)).Length];


            // 手の検出時オブザーバを作成する
            IDisposable OnSourceDetectedObserver = Observable
                .FromEvent<SourceStateEventData>(
                    action => OnSourceDetectedAction += action,
                    action => OnSourceDetectedAction -= action
                )
                .Subscribe(eventData =>
                {
                    // 右手/左手判定が必要なため、検出処理はHandJointUpdatedで行う
                })
                .AddTo(this);

            // 手のロスト時オブザーバを作成する
            IDisposable OnSourceLostObserver = Observable
                .FromEvent<SourceStateEventData>(
                    action => OnSourceLostAction += action,
                    action => OnSourceLostAction -= action
                )
                .Subscribe(eventData =>
                {
                    // ロスト時
                    Debug.Log("Hand : Lost");
                })
                .AddTo(this);

            // 手の更新時オブザーバを作成する
            IDisposable OnHandJointsUpdatedObserver = Observable
                .FromEvent<InputEventData<IDictionary<TrackedHandJoint, MixedRealityPose>>>(
                    action => OnHandJointsUpdatedAction += action,
                    action => OnHandJointsUpdatedAction -= action
                )
                .Where(eventData => eventData.Handedness == GetCurrentHandednessType()) // チェック対象(右手/左手)である場合
                .Select(eventData => CheckCurrentHandStatus(eventData))                 // 手の状態を判定する
                .Buffer(p_HandStatusBufferCount)                                        // 判定結果をバッファする
                .Select(eventDataList => this.RoundingHandStatus(eventDataList))        // バッファした結果から最頻出の状態を取得する
                .Subscribe(status =>
                {
                    // 状態の判定結果
                    p_CurrentHandStatus = status;
                    Debug.Log("HandStatus : " + status.ToString());
                })
                .AddTo(this);
        }

        /// <summary>
        /// 有効時処理
        /// </summary>
        private void OnEnable()
        {
            // ハンドラ登録
            CoreServices.InputSystem?.RegisterHandler<IMixedRealityHandJointHandler>(this);
            CoreServices.InputSystem?.RegisterHandler<IMixedRealitySourceStateHandler>(this);
        }

        /// <summary>
        /// 無効時処理
        /// </summary>
        private void OnDisable()
        {
            // ハンドラ解除
            CoreServices.InputSystem?.UnregisterHandler<IMixedRealityHandJointHandler>(this);
            CoreServices.InputSystem?.UnregisterHandler<IMixedRealitySourceStateHandler>(this);
        }


        /// <summary>
        /// チェックすべき Handedness タイプを返す
        /// </summary>
        /// <returns></returns>
        private Handedness GetCurrentHandednessType()
        {
            Handedness handedness = Handedness.None;
            switch (p_SettingHandType)
            {
                case HandType.Right:
                    handedness = Handedness.Right;
                    break;
                case HandType.Left:
                    handedness = Handedness.Left;
                    break;
                default:
                    break;
            }
            return handedness;
        }


        /// <summary>
        /// 現在の手の状態を判定する
        /// </summary>
        /// <returns></returns>
        private HandStatus CheckCurrentHandStatus(InputEventData<IDictionary<TrackedHandJoint, MixedRealityPose>> eventData)
        {
            // ソースIDを取得する
            p_CurrentHandSourceId = eventData.SourceId;

            // 各指のジョイント位置を取得する
            IDictionary<TrackedHandJoint, MixedRealityPose> joint = eventData.InputData;
            Vector3 wrist_0 = joint[TrackedHandJoint.Wrist].Position;                // 手首
            Vector3 thumb_0 = joint[TrackedHandJoint.ThumbMetacarpalJoint].Position; // 親指手首
            Vector3 thumb_1 = joint[TrackedHandJoint.ThumbProximalJoint].Position;   // 親指第一関節
            Vector3 thumb_2 = joint[TrackedHandJoint.ThumbTip].Position;             // 親指先端
            Vector3 index_0 = joint[TrackedHandJoint.IndexMetacarpal].Position;      // 人指し指手首
            Vector3 index_1 = joint[TrackedHandJoint.IndexKnuckle].Position;         // 人指し指第一関節
            Vector3 index_2 = joint[TrackedHandJoint.IndexTip].Position;             // 人指し指先端
            Vector3 middle_0 = joint[TrackedHandJoint.MiddleMetacarpal].Position;    // 中指手首
            Vector3 middle_1 = joint[TrackedHandJoint.MiddleKnuckle].Position;       // 中指第一関節
            Vector3 middle_2 = joint[TrackedHandJoint.MiddleTip].Position;           // 中指先端
            Vector3 ring_0 = joint[TrackedHandJoint.RingMetacarpal].Position;        // 薬指手首
            Vector3 ring_1 = joint[TrackedHandJoint.RingKnuckle].Position;           // 薬指第一関節
            Vector3 ring_2 = joint[TrackedHandJoint.RingTip].Position;               // 薬指先端
            Vector3 pinky_0 = joint[TrackedHandJoint.PinkyMetacarpal].Position;      // 小指手首
            Vector3 pinky_1 = joint[TrackedHandJoint.PinkyKnuckle].Position;         // 小指第一関節
            Vector3 pinky_2 = joint[TrackedHandJoint.PinkyTip].Position;             // 小指先端


            // 各指の折れ曲がり角
            float angleThumb = Vector3.Angle((thumb_2 - thumb_1).normalized, (thumb_1 - thumb_0).normalized);
            float angleIndex = Vector3.Angle((index_2 - index_1).normalized, (index_1 - index_0).normalized);
            float angleMiddle = Vector3.Angle((middle_2 - middle_1).normalized, (middle_1 - middle_0).normalized);
            float angleRing = Vector3.Angle((ring_2 - ring_1).normalized, (ring_1 - ring_0).normalized);
            float anglePinky = Vector3.Angle((pinky_2 - pinky_1).normalized, (pinky_1 - pinky_0).normalized);


            // 各指の折れ曲がり状態の判定
            bool isHoldThumb = (angleThumb > p_HoldThumbAngle);
            bool isHoldIndex = (angleIndex > p_HoldFingerAngle);
            bool isHoldMiddle = (angleMiddle > p_HoldFingerAngle);
            bool isHoldRing = (angleRing > p_HoldFingerAngle);
            bool isHoldPinky = (anglePinky > p_HoldFingerAngle);


            // 開いている指の数を数える
            int openFingerCount = 0;
            if (!isHoldThumb) openFingerCount++;
            if (!isHoldIndex) openFingerCount++;
            if (!isHoldMiddle) openFingerCount++;
            if (!isHoldRing) openFingerCount++;
            if (!isHoldPinky) openFingerCount++;


            // 手の状態を判定する
            HandStatus status = HandStatus.Normal;

            // 開いている指が 0 本の場合
            if (openFingerCount == 0)
            {
                // グーと判定する
                status = HandStatus.Gur;
            }

            // 開いている指が 1 本の場合
            if (openFingerCount == 1)
            {
                // デフォルトではグーと判定する
                status = HandStatus.Gur;
                if (!isHoldThumb)
                {
                    // 親指が立っていればサムズアップと再判定する
                    status = HandStatus.ThumbsUp;
                }
                if (!isHoldIndex)
                {
                    // 人指し指が立っていればピストルと再判定する
                    status = HandStatus.Pistol;
                }
            }

            // 開いている指が 2 本の場合
            if (openFingerCount == 2)
            {
                // デフォルトではチョキと判定する
                status = HandStatus.Choki;
                if (!isHoldThumb && !isHoldIndex)
                {
                    // 親指と人指し指が立っていればピストルと再判定する
                    status = HandStatus.Pistol;
                }
            }

            // 開いている指が 3 本の場合
            if (openFingerCount == 3)
            {
                // デフォルトではパーと判定する
                status = HandStatus.Par;
                if (!isHoldIndex && !isHoldMiddle)
                {
                    // 人差し指と中指が立っていればチョキと再判定する
                    status = HandStatus.Choki;
                }
            }

            // 開いている指が 4 本の場合
            if (openFingerCount == 4)
            {
                // パーと判定する
                status = HandStatus.Par;
            }

            // 開いている指が 5 本の場合
            if (openFingerCount == 5)
            {
                // パーと判定する
                status = HandStatus.Par;
            }

            return status;
        }


        /// <summary>
        /// 手の状態を最も多区発生した状態で丸め込みを行う
        /// </summary>
        /// <param name="a_HandStatusList"></param>
        /// <returns></returns>
        private HandStatus RoundingHandStatus(IList<HandStatus> a_HandStatusList)
        {
            // 丸め込み結果
            HandStatus roundingResult = HandStatus.Normal;

            // 丸め込み用カウントリストを初期化する
            for (int index = 0; index < p_RoundingCountList.Length; index++)
            {
                p_RoundingCountList[index] = 0;
            }

            // ステータスの最大検出数
            int maxCount = 0;

            foreach (HandStatus checkHandStatus in a_HandStatusList)
            {
                // 各ステータスの判定回数をインクリメントする
                int statusCount = p_RoundingCountList[(int)checkHandStatus]++;

                // 最大検出数のステータスを記録する
                if (statusCount > maxCount)
                {
                    // 最大検出数であればステータスを保持しておく
                    roundingResult = checkHandStatus;
                    maxCount = statusCount;
                }
            }

            return roundingResult;
        }


        // 手の検出時に呼び出すアクション
        private Action<SourceStateEventData> OnSourceDetectedAction;

        // 手のロスト時に呼び出すアクション
        private Action<SourceStateEventData> OnSourceLostAction;

        // 手の更新時に呼び出すアクション
        private Action<InputEventData<IDictionary<TrackedHandJoint, MixedRealityPose>>> OnHandJointsUpdatedAction;

        /// <summary>
        /// 手の検出時に発生するイベント(IMixedRealitySourceStateHandler)
        /// </summary>
        /// <param name="eventData"></param>
        public void OnSourceDetected(SourceStateEventData eventData)
        {
            // アクション呼び出し
            if (OnSourceDetectedAction != null) OnSourceDetectedAction(eventData);
        }

        /// <summary>
        /// 手のロスト時に発生するイベント(IMixedRealitySourceStateHandler)
        /// </summary>
        /// <param name="eventData"></param>
        public void OnSourceLost(SourceStateEventData eventData)
        {
            // アクション呼び出し
            if (OnSourceLostAction != null) OnSourceLostAction(eventData);
        }

        /// <summary>
        /// 手の更新時に発生するイベント(IMixedRealityHandJointHandler)
        /// </summary>
        /// <param name="eventData"></param>
        public void OnHandJointsUpdated(InputEventData<IDictionary<TrackedHandJoint, MixedRealityPose>> eventData)

        {
            // アクション呼び出し
            if (OnHandJointsUpdatedAction != null) OnHandJointsUpdatedAction(eventData);
        }
    }
}

f:id:bluebirdofoz:20210417230004j:plain

このスクリプトを適当なオブジェクトに追加します。
f:id:bluebirdofoz:20210417230017j:plain

動作確認

シーンを再生して動作を確認します。
ログを確認すると、手のジェスチャーを判定したログ結果が表示されています。
f:id:bluebirdofoz:20210417230034j:plain