Skip to content

Commit

Permalink
Expose an API to query if eye tracking is calibrated on HoloLens. (#1…
Browse files Browse the repository at this point in the history
…1664)

Adds the EyeCalibrationChecker helper class with a CalibrationStatus property which tracks whether eyes are present and calibrated. Events are fired when eyes become calibrated or uncalibrated.
  • Loading branch information
marlenaklein-msft authored Jul 4, 2023
1 parent 4427460 commit 04ad752
Show file tree
Hide file tree
Showing 12 changed files with 1,107 additions and 301 deletions.
963 changes: 662 additions & 301 deletions UnityProjects/MRTKDevTemplate/Assets/Scenes/EyeGazeExample.unity

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using UnityEngine;
using UnityEngine.Events;
using Microsoft.MixedReality.Toolkit.Input;
using TMPro;

namespace Microsoft.MixedReality.Toolkit.Examples
{
/// <summary>
/// Checks whether eyes are calibrated and prompts a notification to encourage the user to calibrate.
/// </summary>
[AddComponentMenu("MRTK/Examples/Eye Calibration Notifier")]
public class EyeCalibrationWarning : MonoBehaviour
{
[Tooltip("The EyeCalibrationChecker used to notify the user about calibration status.")]
[SerializeField]
private EyeCalibrationChecker checker;

/// <summary>
/// The EyeCalibrationChecker used to notify the user about calibration status.
/// </summary>
public EyeCalibrationChecker Checker
{
get => checker;
set => checker = value;
}

[Tooltip("The TMP Text used to notify the user about calibration status.")]
[SerializeField]
private TMP_Text text;

/// <summary>
/// The TMP Text used to notify the user about calibration status.
/// </summary>
public TMP_Text Text
{
get => text;
set => text = value;
}

private void OnEnable()
{
if (checker == null)
{
checker = GetComponent<EyeCalibrationChecker>();
}

if (text == null)
{
text = GetComponent<TMP_Text>();
}

if (checker != null)
{
UpdateMessage(checker.CalibratedStatus);
checker.CalibratedStatusChanged.AddListener(UpdateMessageFromEvent);
}
}

private void UpdateMessageFromEvent(EyeCalibrationStatusEventArgs args)
{
UpdateMessage(args.CalibratedStatus);
}

private void UpdateMessage(EyeCalibrationStatus status)
{
string warning = "Eye Calibration Status: Not Calibrated. ";
switch (status)
{
case EyeCalibrationStatus.Unsupported:
text.text = "";
return;
case EyeCalibrationStatus.Calibrated:
text.text = "Eye Calibration Status: Calibrated.";
text.color = Color.green;
return;
case EyeCalibrationStatus.NotCalibrated:
warning += "Please calibrate eye tracking.";
break;
case EyeCalibrationStatus.NotTracked:
warning += "Please ensure this app is granted the appropriate permissions.";
break;
default:
break;
}
text.text = warning;
text.color = Color.red;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.MixedReality.Toolkit.Core.Tests;
using Microsoft.MixedReality.Toolkit.Input.Tests;
using Microsoft.MixedReality.Toolkit.Input;
using NUnit.Framework;
using System.Collections;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.UI;

namespace Microsoft.MixedReality.Toolkit.UX.Runtime.Tests
{
/// <summary>
/// Tests for the EyeCalibrationChecker.
/// </summary>
public class EyeCalibrationCheckerTests : BaseRuntimeInputTests
{
private bool isCalibrated;
private EyeCalibrationStatus calibrationStatus;

[UnityTest]
public IEnumerator TestEyeCalibrationEvents()
{
// Create an EyeCalibrationChecker and add event listeners
GameObject testButton = new GameObject("EyeCalibrationChecker");
EyeCalibrationChecker checker = testButton.AddComponent<EyeCalibrationChecker>();
checker.Calibrated.AddListener(YesEyeCalibration);
checker.NotCalibrated.AddListener(NoEyeCalibration);
checker.CalibratedStatusChanged.AddListener(CalibrationEvent);
yield return null;

// Test whether the events fire when the status is changed
isCalibrated = true;
checker.EditorTestIsCalibrated = EyeCalibrationStatus.Calibrated;
yield return null;
checker.EditorTestIsCalibrated = EyeCalibrationStatus.NotCalibrated;
yield return null;
Assert.IsFalse(isCalibrated, "NotCalibrated event was not fired.");
Assert.AreEqual(calibrationStatus, EyeCalibrationStatus.NotCalibrated, "CalibratedStatusChanged event was not fired.");
yield return null;
checker.EditorTestIsCalibrated = EyeCalibrationStatus.Calibrated;
yield return null;
Assert.IsTrue(isCalibrated, "Calibrated event was not fired.");
Assert.AreEqual(calibrationStatus, EyeCalibrationStatus.Calibrated, "CalibratedStatusChanged event was not fired.");
yield return null;

checker.Calibrated.RemoveListener(NoEyeCalibration);
checker.NotCalibrated.RemoveListener(YesEyeCalibration);
checker.CalibratedStatusChanged.RemoveListener(CalibrationEvent);
}

private void CalibrationEvent(EyeCalibrationStatusEventArgs args)
{
calibrationStatus = args.CalibratedStatus;
}

private void YesEyeCalibration()
{
isCalibrated = true;
}

private void NoEyeCalibration()
{
isCalibrated = false;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions com.microsoft.mrtk.input/Utilities/EyeCalibration.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Runtime.InteropServices.WindowsRuntime;
using UnityEngine;
using UnityEngine.Events;
#if WINDOWS_UWP
using Windows.Perception;
using Windows.Perception.People;
using Windows.Perception.Spatial;
using Windows.UI.Input.Spatial;
#endif

namespace Microsoft.MixedReality.Toolkit.Input
{
/// <summary>
/// A helper class used to check eye calibration status. Only for UWP Platforms.
/// </summary>
[AddComponentMenu("MRTK/Input/Eye Calibration Checker")]
public class EyeCalibrationChecker : MonoBehaviour
{
#region Serialized Fields

/// <summary>
/// For testing purposes, you can manually assign whether eyes are calibrated or not in editor.
/// </summary>
[field: SerializeField, Tooltip("For testing purposes, you can manually assign whether eyes are calibrated or not in editor.")]
public EyeCalibrationStatus EditorTestIsCalibrated = EyeCalibrationStatus.Calibrated;

#endregion Serialized Fields

#region Private Fields

private EyeCalibrationStatus calibrationStatus;

/// <summary>
/// Tracks whether eyes are present and calibrated.
/// </summary>
public EyeCalibrationStatus CalibratedStatus
{
get => calibrationStatus;
}

private EyeCalibrationStatus prevCalibrationStatus;
private const int maxPoseAgeInSeconds = 1;

#endregion Private Fields

#region Events

[SerializeField]
[Tooltip("Event fired when eye tracking is calibrated.")]
private UnityEvent calibrated = new UnityEvent();

/// <summary>
/// Event fired when eye tracking is calibrated.
/// </summary>
public UnityEvent Calibrated => calibrated;

[SerializeField]
[Tooltip("Event fired when eye tracking is not calibrated.")]
private UnityEvent notCalibrated = new UnityEvent();

/// <summary>
/// Event fired when eye tracking is not calibrated.
/// </summary>
public UnityEvent NotCalibrated => notCalibrated;

[SerializeField]
[Tooltip("Event fired whenever eye tracking status changes.")]
private EyeCalibrationStatusEvent calibratedStatusChanged = new EyeCalibrationStatusEvent();

/// <summary>
/// Event fired whenever eye tracking status changes.
/// </summary>
public EyeCalibrationStatusEvent CalibratedStatusChanged => calibratedStatusChanged;

#endregion Events

#region MonoBehaviour Functions
private void Update()
{
if (Application.isEditor)
{
calibrationStatus = EditorTestIsCalibrated;
}
else
{
calibrationStatus = CheckCalibrationStatus();
}

if (prevCalibrationStatus != calibrationStatus)
{
if (calibrationStatus == EyeCalibrationStatus.Calibrated)
{
calibrated.Invoke();
}
else if (calibrationStatus == EyeCalibrationStatus.NotCalibrated)
{
notCalibrated.Invoke();
}
calibratedStatusChanged.Invoke(new EyeCalibrationStatusEventArgs(calibrationStatus));
prevCalibrationStatus = calibrationStatus;
}
}
#endregion MonoBehaviour Functions

#region Private Functions

private EyeCalibrationStatus CheckCalibrationStatus()
{
#if WINDOWS_UWP
if (MixedReality.OpenXR.PerceptionInterop.GetSceneCoordinateSystem(Pose.identity) is SpatialCoordinateSystem worldOrigin)
{
SpatialPointerPose pointerPose = SpatialPointerPose.TryGetAtTimestamp(worldOrigin, PerceptionTimestampHelper.FromHistoricalTargetTime(DateTimeOffset.Now));
if (pointerPose != null)
{
EyesPose eyes = pointerPose.Eyes;
if (eyes != null)
{
// If it's been longer than a second since the last perception snapshot, assume the information has expired.
if ((DateTimeOffset.Now - eyes.UpdateTimestamp.TargetTime).TotalSeconds > maxPoseAgeInSeconds)
{
return EyeCalibrationStatus.NotCalibrated;
}
else if (eyes.IsCalibrationValid)
{
return EyeCalibrationStatus.Calibrated;
}
else
{
return EyeCalibrationStatus.NotCalibrated;
}
}
}
}
return EyeCalibrationStatus.NotTracked;
#else
return EyeCalibrationStatus.Unsupported;
#endif // WINDOWS_UWP
}

#endregion Private Functions
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.MixedReality.Toolkit.Input
{
/// <summary>
/// Used to track the current eye calibration status.
/// </summary>
public enum EyeCalibrationStatus
{
/// <summary>
/// The eye calibration status is not defined.
/// </summary>
Undefined,

/// <summary>
/// The eye calibration status could not be retrieved because this is an unsupported device.
/// </summary>
Unsupported,

/// <summary>
/// The eye calibration status could not be retrieved because eyes are not being tracked.
/// This usually occurs when SpatialPointerPose's Eyes property is null.
/// </summary>
NotTracked,

/// <summary>
/// The eye calibration status was retrieved and eyes are not calibrated.
/// </summary>
NotCalibrated,

/// <summary>
/// The eye calibration status was retrieved and eyes are calibrated.
/// </summary>
Calibrated
};
}
Loading

0 comments on commit 04ad752

Please sign in to comment.