Hololens2手部跟踪 / 手势识别 - MRTK3

这篇博客一方面算是学习笔记,另一方面主要讲述自己是如何通过看官方文档,以及通过GPT来帮助写出脚本实现需求的。
此外打个广告,推荐我以往的博客,我接下来都会出一些系列,主要是给大家一个低门槛高效率开发unity功能模块的方法。

1.问题描述

首先,按惯例,先描述我现在要解决的问题:通过手部做一个动作触发某方法,做另一个动作触发另一个方法。所以我现在需要实现手势识别,或者说手部跟踪。手势设计方面,我的思路就是获取手部关节点位置,然后判断两个关节是否靠近,如果靠近则判定手势触发。这样我认为是比较容易实现的,基于此,我设计了两个手势:

一个是判断左右手食指指尖接触,大拇指指尖接触。另一个是判断左右手小指指尖接触。

这两个手势一方面在一般操作过程中不会误触发,另一方面手势不复杂。

2.官方文档

手部跟踪 - MRTK3 | Microsoft Learn

上面是这次我用的官方文档,其实感觉这个网站很多技术我都用到在了我的项目里,很实用,不过这一次写博客的主要目的是这一功能的官方介绍很“复杂”,说实话,看得我绕绕的。

因为没找到官方案例,所以这一功能属实难懂,而且给了一堆莫名其妙的介绍,但是他有代码行的应用,这算是一个突破口。

就是说如果英语能力好的人,耐心看是不错啦,但是用GPT帮助阅读官方文档我觉得是挺方便的,此外你将官方文档喂给GPT之后,方便它接下来辅助你写脚本。

3.GPT辅助阅读文档以及帮写脚本

下面是我给定的prompt角色设定:

你是Unity3D和Hololens2专家。用中文回答问题,并且注意我没有手,
请回答完整的答案,以便我直接粘贴使用。

下面给出我的向GPT的提问:

// Get a reference to the aggregator.
var aggregator = XRSubsystemHelpers.GetFirstRunningSubsystem<HandsAggregatorSubsystem>();
// Wait until an aggregator is available.
IEnumerator EnableWhenSubsystemAvailable()
{
    yield return new WaitUntil(() => XRSubsystemHelpers.GetFirstRunningSubsystem<HandsAggregatorSubsystem>() != null);
    GoAhead();
}
// Get a single joint (Index tip, on left hand, for example)
bool jointIsValid = aggregator.TryGetJoint(TrackedHandJoint.IndexTip, XRNode.LeftHand, out HandJointPose jointPose);
// Get an entire hand's worth of joints from the left hand.
bool allJointsAreValid = aggregator.TryGetEntireHand(XRNode.LeftHand, out IReadOnlyList<HandJointPose> joints)
// Check whether the user's left hand is facing away (commonly used to check "aim" intent)
// This is adjustable with the HandFacingAwayTolerance option in the Aggregator configuration.
// "handIsValid" represents whether there was valid hand data in the first place!
bool handIsValid = aggregator.TryGetPalmFacingAway(XRNode.LeftHand, out bool isLeftPalmFacingAway)
// Query pinch characteristics from the left hand.
// pinchAmount is [0,1], normalized to the open/closed thresholds specified in the Aggregator configuration.
// "isReadyToPinch" is adjusted with the HandRaiseCameraFOV and HandFacingAwayTolerance settings in the configuration.
bool handIsValid = aggregator.TryGetPinchProgress(XRNode.LeftHand, out bool isReadyToPinch, out bool isPinching, out float pinchAmount)
这是关于手部跟踪的官方文档内容,请帮我解释相关内容的功能。

可以通过上述提问快速了解代码结构。接着提问:

我现在需要获取右手指尖的关节点IndexTip的位置位置信息,并实时打印出来。请帮我写一个脚本,实现以上功能。

这么问主要是来测试GPT是否可以完成这部分简单的功能。如果你直接让他输出一个代码,你自己可能都不知道它的运行机制,而且如果运行失败了你也不知道哪里出错了【血泪教训】。所以我喜欢一步步来给出自己的需求。先一步步提问,保证每次GPT给出的答案的正确性,这样一来可以及时发现是哪一个功能添加时出了问题,针对性的让GPT来debug,是的没错,这个逻辑就是让GPT来debug。

注意:关于上面代码可能会出现诸如没有该域名的using,直接自动化using就行。

然后在接着问GPT之前,先上一张我在其他博客看到的MRTK2的手部关节图。

这个关节图其实MRTK2和MRTK3有细微差别,但是其实问题不大,如下面这行代码

aggregator.TryGetJoint(TrackedHandJoint.IndexTip, XRNode.RightHand, out HandJointPose rightIndexPose)

需要自己注意的地方就是IndexTip这个索引的使用,在不确定GPT能否正确说出索引的情况下,我选择将这个索引直接告诉它,比如提问GPT:我需要获取左右手ThumbTip位置,IndexTip位置,以及PinkyTip位置。但是你会发现代码报错,说PinkyTip没找到,其实是小指这个索引变了的缘故,直接在输入TrackedHandJoint.后,后面会自动弹出所有关节点索引,我看到LittleTip,猜测应该就是小指尖,结果测试果然没错。

最后接着提问,内容就大概关于:你希望提取_______哪几个关节点位置,然后判定____和____的距离达到某一阈值后,触发相关功能函数。

以下是我的代码,供大家直接拿去使用。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
using Microsoft.MixedReality.Toolkit.Subsystems;
using Microsoft.MixedReality.Toolkit;
using UnityEngine.Events;

public class HandTipTracker : MonoBehaviour
{
    private HandsAggregatorSubsystem aggregator;
    private float lastGestureTime = 0.0f;
    public UnityEvent start;
    public UnityEvent close;
    public UnityEvent onGesture_Close;
    // 状态符号:0代表上次触发关闭功能,1代表上次触发开启功能
    private int gestureState = 0;

    void Start()
    {
        StartCoroutine(EnableWhenSubsystemAvailable());
    }

    IEnumerator EnableWhenSubsystemAvailable()
    {
        yield return new WaitUntil(() => XRSubsystemHelpers.GetFirstRunningSubsystem<HandsAggregatorSubsystem>() != null);
        aggregator = XRSubsystemHelpers.GetFirstRunningSubsystem<HandsAggregatorSubsystem>();
    }

    void Update()
    {
        if (aggregator != null)
        {
            if (aggregator.TryGetJoint(TrackedHandJoint.IndexTip, XRNode.RightHand, out HandJointPose rightIndexPose))
            {
                //Debug.Log($"右手指尖位置: {rightIndexPose.Position}");
            }
            if (aggregator.TryGetJoint(TrackedHandJoint.IndexTip, XRNode.LeftHand, out HandJointPose leftIndexPose))
            {
                //Debug.Log($"左手食指尖位置: {leftIndexPose.Position}");
            }
            if (aggregator.TryGetJoint(TrackedHandJoint.ThumbTip, XRNode.LeftHand, out HandJointPose leftThumbPose))
            {
               // Debug.Log($"左手大拇指尖位置: {leftThumbPose.Position}");
            }
            if (aggregator.TryGetJoint(TrackedHandJoint.ThumbTip, XRNode.RightHand, out HandJointPose rightThumbPose))
            {
               // Debug.Log($"右手大拇指尖位置: {rightThumbPose.Position}");
            }
            if (aggregator.TryGetJoint(TrackedHandJoint.LittleTip, XRNode.LeftHand, out HandJointPose leftLittlePose)) { }

            if (aggregator.TryGetJoint(TrackedHandJoint.LittleTip, XRNode.RightHand, out HandJointPose rightLittlePose)) { }

            CheckGesture_Open(leftIndexPose, rightIndexPose, leftThumbPose, rightThumbPose);
            CheckGesture_Close(rightLittlePose, leftLittlePose);
        }
    }

    private void CheckGesture_Open(HandJointPose leftIndexPose, HandJointPose rightIndexPose, HandJointPose leftThumbPose, HandJointPose rightThumbPose)
    {
        // 判定指尖接触的阈值(需要根据实际情况调整)
        float touchThreshold = 0.05f;

        // 判断食指尖是否接触
        bool isIndexTipsTouching = Vector3.Distance(leftIndexPose.Position, rightIndexPose.Position) < touchThreshold;

        // 判断大拇指尖是否接触
        bool isThumbTipsTouching = Vector3.Distance(leftThumbPose.Position, rightThumbPose.Position) < touchThreshold;

        // 如果食指尖和大拇指尖都接触
        if (isIndexTipsTouching && isThumbTipsTouching)
        {
            // 当前时间
            float currentTime = Time.time;

            // 如果当前时间与上次手势检测时间的差大于2秒
            if (currentTime - lastGestureTime > 2.0f)
            {
                // 更新上次手势检测时间
                lastGestureTime = currentTime;

                // 触发相应的功能
                ToggleGestureState();
            }
        }
    }
    private void CheckGesture_Close(HandJointPose leftLittlePose, HandJointPose rightLittlePose)
    {
        // 判定指尖接触的阈值
        float touchThreshold = 0.05f;

        bool isPinkyTipsTouching = Vector3.Distance(leftLittlePose.Position, rightLittlePose.Position) < touchThreshold;

        // 如果小拇指尖靠近
        if (isPinkyTipsTouching)
        {
            // 当前时间
            float currentTime = Time.time;

            // 如果当前时间与上次手势检测时间的差大于2秒
            if (currentTime - lastGestureTime > 2.0f)
            {
                // 更新上次手势检测时间
                lastGestureTime = currentTime;

                // 触发相应的功能
                OnGesture_Close();
            }
        }
     
    }
    private void ToggleGestureState()
    {
        if (gestureState == 0)
        {
            gestureState = 1;
            Debug.Log("开启功能触发");
            start?.Invoke();
        }
        else
        {
            gestureState = 0;
            Debug.Log("关闭功能触发");
            close?.Invoke();
        }
    }
    private void OnGesture_Close()
    {
        Debug.Log("检测到手势:左右手小拇指尖接触");
        // 触发UnityEvent
        onGesture_Close?.Invoke();
    }
}

这个代码需要注意的点是,我才用的是UnityEvent事件,从Unity界面中添加事件,这样的好处就是更加灵活,而且后期阅读代码时更直观的知道这个脚本是干嘛的。毕竟Unity就是实时判断各种状态机的状态来判断是否调用其他函数的过程。只要知道状态的定义以及函数对象是谁,基本这个代码就知道咋回事了。

而至于这个gestureState符号主要是实现一个手势控制功能的开关。

4.问题总结,小tip

这个脚本的测试环节:

  • 平时习惯按空格键操控一只手,这两只手的情况就是按下ctrl键和shift键,单独移动左手实现的。
  • 此外还要注意,在电脑上运行代码时,一定要从一开始就将虚拟手显示出来,因为系统在start时,没看到手的情况下会默认它满足所有关节点的接触的。只要显示一只手后,这个bug就会消除。用头显设备时我也建议在刚开始时保证手在视线内。

  • 14
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
本篇学习笔记将介绍HoloLens 2 的 EyeTracking(眼动跟踪)和语音识别功能,以及如何在MRTK(Mixed Reality Toolkit)中进行开发。 EyeTracking HoloLens 2 的 EyeTracking 功能可以让用户通过视线在应用程序中进行选择和交互,而无需使用手势或语音命令。在MRTK中,我们可以使用EyeTrackingProvider来获取眼动跟踪数据。 首先,我们需要在MRTK中启用EyeTrackingProvider。在Unity中打开MRTK的配置文件,选择Eye Tracking选项卡,勾选Enable Eye Tracking选项,并将Update Interval设置为0.01。 接下来,在我们的场景中添加一个Cube,并将其设置为Interactable,这样用户可以使用眼动跟踪来选择它。然后,我们需要将EyeTrackingTarget组件添加到Cube上,这将使其成为眼动跟踪的目标。 最后,在我们的脚本中,我们可以使用EyeTrackingProvider来获取当前视线所在的位置,并将其用于交互。例如,我们可以在Update函数中检查是否正在注视着目标,并执行相应的操作: ```csharp using UnityEngine; using Microsoft.MixedReality.Toolkit.Input; public class EyeTrackingInteraction : MonoBehaviour { private EyeTrackingTarget target; private void Start() { target = GetComponent<EyeTrackingTarget>(); } private void Update() { if (target.IsBeingLookedAt) { // Do something } } } ``` 语音识别 HoloLens 2 的语音识别功能可以让用户通过语音命令在应用程序中进行选择和交互。在MRTK中,我们可以使用SpeechInputHandler来处理语音输入。 首先,我们需要在MRTK的配置文件中启用语音识别,选择Input选项卡,勾选Enable Speech Input选项,并选择我们要识别的语言。 接下来,在我们的场景中添加一个Cube,并将其设置为Interactable,这样用户可以使用语音命令来选择它。然后,我们需要将SpeechInputHandler组件添加到Cube上,这将使其能够接收语音输入。 最后,在我们的脚本中,我们可以使用SpeechInputHandler来处理语音输入,并将其用于交互。例如,我们可以在OnSpeechKeywordRecognized函数中检查识别到的关键字,并执行相应的操作: ```csharp using UnityEngine; using Microsoft.MixedReality.Toolkit.Input; public class SpeechInteraction : MonoBehaviour { private void Start() { var speechInputHandler = GetComponent<SpeechInputHandler>(); speechInputHandler.OnSpeechKeywordRecognized += OnSpeechKeywordRecognized; } private void OnSpeechKeywordRecognized(SpeechEventData eventData) { if (eventData.Command.Keyword == "select") { // Do something } } } ``` 总结 HoloLens 2 的 EyeTracking 和语音识别功能可以使用户更加方便地与应用程序交互。在MRTK中,我们可以使用EyeTrackingProvider和SpeechInputHandler来处理眼动跟踪和语音输入。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值