Help us understand the problem. What is going on with this article?

OculusQuest ハンドトラッキングSDKから、指Boneの情報を取得し分析する

最近のOculusQuestさんちょっと攻めすぎですよね。
OculusLinkのβが出たのもつい最近な気がしているのに、さらにハンドトラッキングまで!!!

そして、2019/12/20にはUnityのAssetStoreにハンドトラッキング対応のSDKが配布されました。 高速感!

導入方法なんかについては他の方の記事にお任せして・・・。

肝はOVRSkelton

OVRHand には GetFingerIsPinching()GetFingerPinchStrength() があり、それぞれ、ピンチ(つまんでいる)状態かどうかと
つまんでいる力(距離?)が取得できるメソッドが入っています。
参考:OculusQuestのハンドトラッキングについて色々調べてみた

しかし、ぱっと見たところそれ以外の情報取得がそんなにありません。

そこで上記記事でも言及していますが、もっと詳しい情報を知りたい場合は OVRSkelton.Bones にボーン情報が入っている のでそれを使うことにします。

というのも

僕がやりたかったのは以前作った 「空中に図形を描いて物体を生成するアプリを作ってみた」 のハンドトラッキング版だったので、空中に絵を描くイメージです。
みなさんが空中に絵を描く(例えば、誰かに「**って漢字どう書くんだったっけ?」と聞かれた)時に指をどんな形にするかっていうと

十中八九:point_up:←これですよね

この「人差し指だけを伸ばして、他曲げている」をGetFingerIsPinching()GetFingerPinchStrength()でやるのはちょっと無理があります。(できなくはないと思いますが)

そこで、 OVRSkelton.Bones から情報を抜き出して簡単に分析することにしました。

Bone情報取得

OVRSkelton.Bones は IList(Readonly)で、何番目にどの情報が入っているかは以下のようにenumで宣言されています。

    public enum BoneId
    {
        Invalid                 = OVRPlugin.BoneId.Invalid,

        Hand_Start              = OVRPlugin.BoneId.Hand_Start,
        Hand_WristRoot          = OVRPlugin.BoneId.Hand_WristRoot,          // root frame of the hand, where the wrist is located
        Hand_ForearmStub        = OVRPlugin.BoneId.Hand_ForearmStub,        // frame for user's forearm
        Hand_Thumb0             = OVRPlugin.BoneId.Hand_Thumb0,             // thumb trapezium bone
        Hand_Thumb1             = OVRPlugin.BoneId.Hand_Thumb1,             // thumb metacarpal bone
        Hand_Thumb2             = OVRPlugin.BoneId.Hand_Thumb2,             // thumb proximal phalange bone
        Hand_Thumb3             = OVRPlugin.BoneId.Hand_Thumb3,             // thumb distal phalange bone
        Hand_Index1             = OVRPlugin.BoneId.Hand_Index1,             // index proximal phalange bone
        Hand_Index2             = OVRPlugin.BoneId.Hand_Index2,             // index intermediate phalange bone
        Hand_Index3             = OVRPlugin.BoneId.Hand_Index3,             // index distal phalange bone
        Hand_Middle1            = OVRPlugin.BoneId.Hand_Middle1,            // middle proximal phalange bone
        Hand_Middle2            = OVRPlugin.BoneId.Hand_Middle2,            // middle intermediate phalange bone
        Hand_Middle3            = OVRPlugin.BoneId.Hand_Middle3,            // middle distal phalange bone
        Hand_Ring1              = OVRPlugin.BoneId.Hand_Ring1,              // ring proximal phalange bone
        Hand_Ring2              = OVRPlugin.BoneId.Hand_Ring2,              // ring intermediate phalange bone
        Hand_Ring3              = OVRPlugin.BoneId.Hand_Ring3,              // ring distal phalange bone
        Hand_Pinky0             = OVRPlugin.BoneId.Hand_Pinky0,             // pinky metacarpal bone
        Hand_Pinky1             = OVRPlugin.BoneId.Hand_Pinky1,             // pinky proximal phalange bone
        Hand_Pinky2             = OVRPlugin.BoneId.Hand_Pinky2,             // pinky intermediate phalange bone
        Hand_Pinky3             = OVRPlugin.BoneId.Hand_Pinky3,             // pinky distal phalange bone
        Hand_MaxSkinnable       = OVRPlugin.BoneId.Hand_MaxSkinnable,
        // Bone tips are position only. They are not used for skinning but are useful for hit-testing.
        // NOTE: Hand_ThumbTip == Hand_MaxSkinnable since the extended tips need to be contiguous
        Hand_ThumbTip           = OVRPlugin.BoneId.Hand_ThumbTip,           // tip of the thumb
        Hand_IndexTip           = OVRPlugin.BoneId.Hand_IndexTip,           // tip of the index finger
        Hand_MiddleTip          = OVRPlugin.BoneId.Hand_MiddleTip,          // tip of the middle finger
        Hand_RingTip            = OVRPlugin.BoneId.Hand_RingTip,            // tip of the ring finger
        Hand_PinkyTip           = OVRPlugin.BoneId.Hand_PinkyTip,           // tip of the pinky
        Hand_End                = OVRPlugin.BoneId.Hand_End,

        // add new bones here

        Max                     = OVRPlugin.BoneId.Max
    }

親指:Thumb
人差し指:Index
中指:Middle
薬指:Ring
小指:Pinky

で、1,2,3 は関節を表しており、数字が大きくなるほど指先に近づいていく ようです。
僕は英語をロクに読まず 「第一関節」= 1 だと思い込んで処理を書いていたらさっぱり正しい値が返ってこなくてハテ? となりました。ご注意を・・・。
そして、 Tip とついているのは指先です。

分かりやすくマッピングしてみると、どうもこんな感じ・・・?
image.png

なお、Pinky と Thumb だけ 0番があります。

そして、この Bone には Transform が入っており、位置や回転が取れそうです。
例えば「人差し指の先端の位置」を取るには

var indexTipPos = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_IndexTip].Transform.position;

こうなります(注:enumなのでintキャストが必要)

直線判定

以上を踏まえ、:point_up:この「人差し指がまっすぐになっていて、中指、薬指、小指は曲がっている」を判定します。(親指は除きました。)

この「人差し指がまっすぐになっている」というのは人差し指の「第三関節→第二関節」の方向(ベクトル)と、「第二関節→第一関節」の方向(ベクトル)と「第一関節→指先」の方向(ベクトル)が大体同じ方向を向いている ということです。

この「二つのベクトルが大体同じ方向を向いている」=「どれぐらい似通っているか」を表すのは。そう内積(Vector3.Dot)です。

ベクトルの内積は直角(一番似通っていない)な場合は0
全く同じ場合は+1
全く逆方向の場合は-1です

これを人差し指だけではなく他の指の分もベタ書きするとそこそこ長くなってしまうので、以下のようなメソッドを用意すると便利だと思います。

        [SerializeField]
        private OVRSkeleton _skeleton; //右手、もしくは左手の Bone情報

        /// <summary>
        /// 指定した全てのBoneIDが直線状にあるかどうか調べる
        /// </summary>
        /// <param name="threshold">閾値 1に近いほど厳しい</param>
        /// <param name="boneids"></param>
        /// <returns></returns>
        private bool IsStraight(float threshold, params OVRSkeleton.BoneId[] boneids)
        {
            if (boneids.Length < 3) return false;   //調べようがない
            Vector3? oldVec = null;
            var dot = 1.0f;
            for (var index = 0; index < boneids.Length-1; index++)
            {
                var v = (_skeleton.Bones[(int)boneids[index+1]].Transform.position - _skeleton.Bones[(int)boneids[index]].Transform.position).normalized;
                if (oldVec.HasValue)
                {
                    dot *= Vector3.Dot(v, oldVec.Value); //内積の値を総乗していく
                }
                oldVec = v;//ひとつ前の指ベクトル
            }
            return dot >= threshold; //指定したBoneIDの内積の総乗が閾値を超えていたら直線とみなす
        }

可変長配列でBoneIDを複数(3個以上)受けとり、一つ前のBoneIDが示す関節から関節のベクトルと、今のBoneIDが示す関節と次の関節のベクトルの内積を計算して、 dot にどんどん乗算していっています。(別に平均でも良い気はしますが)

これを使うと、人差し指がまっすぐかどうかを判定してLogに表示する場合、このようになります。

var isIndexStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Index1, OVRSkeleton.BoneId.Hand_Index2, OVRSkeleton.BoneId.Hand_Index3, OVRSkeleton.BoneId.Hand_IndexTip);
Debug.Log($"人差し指は{isIndexStraight?"まっすぐ":"曲がってる"}");

おなじく、他の指も調べていけば、:point_up:「人差し指がまっすぐになっていて、中指、薬指、小指は曲がっている」は判定できそうです。

var isIndexStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Index1, OVRSkeleton.BoneId.Hand_Index2, OVRSkeleton.BoneId.Hand_Index3, OVRSkeleton.BoneId.Hand_IndexTip);
var isMiddleStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Middle1, OVRSkeleton.BoneId.Hand_Middle2, OVRSkeleton.BoneId.Hand_Middle3, OVRSkeleton.BoneId.Hand_MiddleTip);
var isRingStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Ring1, OVRSkeleton.BoneId.Hand_Ring2, OVRSkeleton.BoneId.Hand_Ring3, OVRSkeleton.BoneId.Hand_RingTip);
var isPinkyStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Pinky0, OVRSkeleton.BoneId.Hand_Pinky1, OVRSkeleton.BoneId.Hand_Pinky2, OVRSkeleton.BoneId.Hand_Pinky3, OVRSkeleton.BoneId.Hand_PinkyTip);

Debug.Log($"人差し指は{isIndexStraight?"まっすぐ":"曲がってる"}");
Debug.Log($"中指は{isMiddleStraight?"まっすぐ":"曲がってる"}");
Debug.Log($"薬指は{isRingStraight?"まっすぐ":"曲がってる"}");
Debug.Log($"小指は{isPinkyStraight?"まっすぐ":"曲がってる"}");

if(isIndexStraight && !isMiddleStraight  && !isRingStraight  && !isPinkyStraight ){ //人差し指だけまっすぐで、その他が曲がっている
    Debug.Log($"お前がナンバーワンだ!");
}

そして、人差し指の先端の位置 (IndexTip) で線を描くとこうなりました

うーん。 ノイズなのかなんなのか。 まったく直線が描けてないですね。

とりあえずの目標(:point_up:で線を描く)はできているので、よしとします。
この問題はまた後日・・・。

まとめ

OVRHandからとれるピンチ情報に加え、この「各指が曲がっているか(false)伸びているか(true)」が加わるだけでもいろんな事が出来るんじゃないかなと思います。
<例>
- 全部falseならグー、人差し指と中指だけtrueならチョキ、全部trueならパー、でじゃんけん
- 中指と小指がfalse、そのほかがtrueで「グワシ!」
- 中指だけtrue そのほかfalse で 「F******!」で、プログラム強制終了。
などなど。

しかし・・。ちゃんとリファレンス見てないので、こんなことしなくても情報は取れるよ!もっといい方法あるよ! などあったらコメント教えてください。

ではでは、よきOculusQuestライフを。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした