microsoft / MixedRealityToolkit-Unity

This repository is for the legacy Mixed Reality Toolkit (MRTK) v2. For the latest version of the MRTK please visit https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity
https://aka.ms/mrtkdocs
MIT License
6.01k stars 2.12k forks source link

Eye tracking not working reliably for the HoloLens 2 #8816

Closed RashKash103 closed 3 years ago

RashKash103 commented 3 years ago

Describe the bug

Eye tracking does not work consistently for the Microsoft HoloLens 2. The same exact code seems to work sometimes but does not work most of the time. For example, the application may work completely fine when run once, but half an hour later, the eye tracking stops working until, perhaps, the next day. To be clear, this only affects the application itself. To my knowledge, the eye tracking within the OS outside of the application is unaffected.

Edit: After some further experimentation, I stumbled across the WindowsMixedRealityEyeGazeDataProvider.cs file. The appropriate function is the one below. It seems that in the line var eyes = pointerPose.Eyes, eyes gets set to null, thus not continuing into the if (eyes != null) statement. From the documentation, it appears that this SpatialPointerPose.Eyes would return null if on an unsupported device, but the HoloLens 2 device definitely supports eye tracking.

        public override void Update()
        {
#if (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP
            using (UpdatePerfMarker.Auto())
            {
                if (WindowsMixedRealityUtilities.SpatialCoordinateSystem == null || !eyesApiAvailable)
                {
                    return;
                }

                SpatialPointerPose pointerPose = SpatialPointerPose.TryGetAtTimestamp(WindowsMixedRealityUtilities.SpatialCoordinateSystem, PerceptionTimestampHelper.FromHistoricalTargetTime(DateTimeOffset.Now));
                if (pointerPose != null)
                {
                    var eyes = pointerPose.Eyes;
                    if (eyes != null)
                    {
                        Service?.EyeGazeProvider?.UpdateEyeTrackingStatus(this, eyes.IsCalibrationValid);

                        if (eyes.Gaze.HasValue)
                        {
                            Ray newGaze = new Ray(eyes.Gaze.Value.Origin.ToUnityVector3(), eyes.Gaze.Value.Direction.ToUnityVector3());

                            if (SmoothEyeTracking)
                            {
                                newGaze = SmoothGaze(newGaze);
                            }

                            Service?.EyeGazeProvider?.UpdateEyeGaze(this, newGaze, eyes.UpdateTimestamp.TargetTime.UtcDateTime);
                        }
                    }
                }
            }
#endif // (UNITY_WSA && DOTNETWINRT_PRESENT) || WINDOWS_UWP
        }

To reproduce

Code to reproduce the behavior:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
using System.Linq;
using Definitions;
using TMPro;
using Microsoft.MixedReality.Toolkit.Input;
using Microsoft.MixedReality.Toolkit;

public class EyeTracker : MonoBehaviour {

    #region Unity Functions

    private void Start() {
        StartCoroutine(QueryGaze());
    }

    #endregion

    #region Private Functions

    /// <summary>
    /// Queries the gaze.
    /// </summary>
    /// <returns>Coroutine object.</returns>
    private IEnumerator QueryGaze() {

        yield return new WaitForSeconds(3f);
        yield return null;

        // Waits until the eye tracking initializes.
        // CoreServices.InputSystem.EyeGazeProvider.IsEyeTrackingDataValid is true here because it passes this statement
        yield return new WaitUntil(() => CoreServices.InputSystem.EyeGazeProvider.IsEyeTrackingDataValid);
        EyeTrackerEnabled = true;
        Debug.Log("Eye tracking is now enabled!");

        // While the eye tracker is enabled
        while (EyeTrackerEnabled) {
            yield return new WaitForEndOfFrame(); // <-- I've tried with and without this
            // I've tried this as well, it just hangs here
            // yield return new WaitUntil(() => CoreServices.InputSystem.EyeGazeProvider.IsEyeTrackingDataValid);

            // Always prints false here
            Debug.Log(CoreServices.InputSystem.EyeGazeProvider.IsEyeTrackingDataValid);

            // ... //

            yield return null;
        }
    }

    #endregion

}

Expected behavior

Within the while loop, I expect CoreServices.InputSystem.EyeGazeProvider.IsEyeTrackingDataValid to be true and the eye tracking to be working as expected.

What I've Tried

Your setup

Target platform

Additional context

Interestingly, this code has worked pretty reliably in the past in the same project. Other, unrelated code has been added to the project but that could not have broken the eye tracking as it has remained untouched for some time.

Additionally, for our use case, falling back to head-based gaze tracking is not an option!

RashKash103 commented 3 years ago

Seems like this issue is due to a negative interaction between the eye tracker and the Microsoft.MixedReality.QR NuGet package that is used to scan QR codes on the HoloLens device. When the QR code reader is initialized early on (presumably before or immediately after the eye tracker initializes), the eye tracker stops working as soon as the reader is is closed. The intermittent behavior was presumably due to timing differences and the order of execution when the Unity application was starting up.

To fix this issue, I added a yield return new WaitForSeconds(5f) before calling QRCodeWatcher.Start(). Since this addition, the eye tracker has been working 100% reliably.