Расширенное отслеживание взгляда в собственном движке

Расширенное отслеживание взгляда — это новая возможность в HoloLens 2. Это супермножество стандартного отслеживания взгляда, которое предоставляет только объединенные данные о взгляде. Расширенное отслеживание взгляда также предоставляет отдельные данные о взгляде и позволяет приложениям задавать разные частоты кадров для данных взгляда, например 30, 60 и 90 кадров в секунду. В настоящее время HoloLens 2 не поддерживают другие функции, такие как открытость глаз и вергентность глаз.

Пакет SDK для расширенного отслеживания глаз позволяет приложениям получать доступ к данным и функциям расширенного отслеживания глаз. Его можно использовать вместе с API WinRT или API OpenXR.

В этой статье рассматриваются способы использования расширенного пакета SDK для отслеживания взгляда в собственном обработчике (C# или C++/WinRT) вместе с API-интерфейсами WinRT.

Настройка проекта

  1. Holographic DirectX 11 App (Universal Windows)Создайте проект или Holographic DirectX 11 App (Universal Windows) (C++/WinRT) с помощью Visual Studio 2019 или более поздней версии или откройте существующий голографический проект Visual Studio.
  2. Импортируйте пакет SDK для расширенного отслеживания глаз в проект.
    1. В Обозреватель решений Visual Studio щелкните правой кнопкой мыши проект —> Управление пакетами NuGet...
    2. Убедитесь, что источник пакета в правом верхнем углу указывает на nuget.org: https://api.nuget.org/v3/index.json
    3. Откройте вкладку Браузер и выполните поиск Microsoft.MixedReality.EyeTracking.
    4. Нажмите кнопку Установить, чтобы установить последнюю версию пакета SDK.
      Снимок экрана пакета NuGet пакета SDK для отслеживания глаз.
  3. Настройка возможности ввода взгляда
    1. Дважды щелкните файл Package.appxmanifest в Обозреватель решений.
    2. Перейдите на вкладку Возможности и проверка входные данные взгляда.
  4. Включите головной файл и используйте пространство имен.
    • Для проекта C#:
    using Microsoft.MixedReality.EyeTracking;
    
    • Для проекта C++/WinRT:
    #include <winrt/Microsoft.MixedReality.EyeTracking.h>
    using namespace winrt::Microsoft::MixedReality::EyeTracking;
    
  5. Используйте API-интерфейсы расширенного пакета SDK для отслеживания глаз и реализуйте логику.
  6. Сборка и развертывание в HoloLens.

Общие сведения о действиях для получения данных о взгляде

Для получения данных о взгляде с помощью API-интерфейсов пакета SDK для расширенного отслеживания глаз необходимо выполнить следующие действия.

  1. Получите доступ к функциям отслеживания взгляда у пользователя.
  2. Следите за подключениями и отключениями средства отслеживания взгляда.
  3. Откройте средство отслеживания взгляда и запросите его возможности.
  4. Многократное чтение данных взгляда из средства отслеживания взгляда.
  5. Передача данных взгляда в другие SpatialCoordinateSystems.

Получение доступа к функциям отслеживания взгляда

Чтобы использовать любую информацию, связанную с глазами, приложение должно сначала запросить согласие пользователя.

var status = await Windows.Perception.People.EyesPose.RequestAccessAsync();
bool useGaze = (status == Windows.UI.Input.GazeInputAccessStatus.Allowed);
auto accessStatus = co_await winrt::Windows::Perception::People::EyesPose::RequestAccessAsync();
bool useGaze = (accessStatus.get() == winrt::Windows::UI::Input::GazeInputAccessStatus::Allowed);

Обнаружение средства отслеживания взгляда

Обнаружение средства отслеживания взгляда осуществляется с помощью EyeGazeTrackerWatcher класса . EyeGazeTrackerAdded События и EyeGazeTrackerRemoved соответственно возникают при обнаружении или отключении средства отслеживания взгляда.

Наблюдатель должен быть явно запущен с StartAsync() помощью метода , который завершается асинхронно, когда уже подключенные средства отслеживания были сигнализируют через EyeGazeTrackerAdded событие .

При обнаружении EyeGazeTracker средства отслеживания взгляда экземпляр передается приложению в EyeGazeTrackerAdded параметрах события; при отключении средства отслеживания соответствующий EyeGazeTracker экземпляр передается в событие EyeGazeTrackerRemoved.

EyeGazeTrackerWatcher watcher = new EyeGazeTrackerWatcher();
watcher.EyeGazeTrackerAdded += _watcher_EyeGazeTrackerAdded;
watcher.EyeGazeTrackerRemoved += _watcher_EyeGazeTrackerRemoved;
await watcher.StartAsync();
...

private async void _watcher_EyeGazeTrackerAdded(object sender, EyeGazeTracker e)
{
    // Implementation is in next section
}

private void _watcher_EyeGazeTrackerRemoved(object sender, EyeGazeTracker e)
{
    ...
}
EyeGazeTrackerWatcher watcher;
watcher.EyeGazeTrackerAdded(std::bind(&SampleEyeTrackingNugetClientAppMain::OnEyeGazeTrackerAdded, this, _1, _2));
watcher.EyeGazeTrackerRemoved(std::bind(&SampleEyeTrackingNugetClientAppMain::OnEyeGazeTrackerRemoved, this, _1, _2));
co_await watcher.StartAsync();
...

winrt::Windows::Foundation::IAsyncAction SampleAppMain::OnEyeGazeTrackerAdded(const EyeGazeTrackerWatcher& sender, const EyeGazeTracker& tracker)
{
    // Implementation is in next section
}
void SampleAppMain::OnEyeGazeTrackerRemoved(const EyeGazeTrackerWatcher& sender, const EyeGazeTracker& tracker)
{
    ...
}

Средство отслеживания открытого взгляда

При получении EyeGazeTracker экземпляра приложение сначала должно открыть его, вызвав OpenAsync() метод . Затем он может запросить возможности средства отслеживания, если это необходимо. Метод OpenAsync() принимает логический параметр. Это указывает, требуется ли приложению доступ к функциям, не относящимся к стандартному отслеживанию взгляда, например к отдельным векторам взгляда или изменению частоты кадров средства отслеживания.

Комбинированный взгляд является обязательной функцией, поддерживаемой всеми средствами отслеживания взгляда. Другие функции, такие как доступ к отдельному взгляду, являются необязательными и могут поддерживаться или не поддерживаться в зависимости от средства отслеживания и его драйвера. Для этих необязательных функций класс предоставляет свойство, указывающее, EyeGazeTracker поддерживается ли эта функция, например свойство , которое указывает, AreLeftAndRightGazesSupported поддерживается ли устройство отдельными сведениями о взгляде.

Вся пространственная информация, предоставленная средство отслеживания взгляда, публикуется в отношении самого средства отслеживания, который определяется идентификатором динамического узла. Использование nodeId для получения SpatialCoordinateSystem с помощью API WinRT может преобразовать координаты данных взгляда в другую систему координат.

private async void _watcher_EyeGazeTrackerAdded(object sender, EyeGazeTracker e)
{
    try
    {
        // Try to open the tracker with access to restricted features
        await e.OpenAsync(true);

        // If it has succeeded, store it for future use
        _tracker = e;

        // Check support for individual eye gaze
        bool supportsIndividualEyeGaze = _tracker.AreLeftAndRightGazesSupported;

        // Get a spatial locator for the tracker, this will be used to transfer the gaze data to other coordinate systems later
        var trackerNodeId = e.TrackerSpaceLocatorNodeId;
        _trackerLocator = Windows.Perception.Spatial.Preview.SpatialGraphInteropPreview.CreateLocatorForNode(trackerNodeId);
    }
    catch (Exception ex)
    {
        // Unable to open the tracker
    }
}
winrt::Windows::Foundation::IAsyncAction SampleEyeTrackingNugetClientAppMain::OnEyeGazeTrackerAdded(const EyeGazeTrackerWatcher&, const EyeGazeTracker& tracker)
{
   auto newTracker = tracker;

   try
   {
        // Try to open the tracker with access to restricted features
        co_await newTracker.OpenAsync(true);

        // If it has succeeded, store it for future use
        m_gazeTracker = newTracker;

        // Check support for individual eye gaze
        const bool supportsIndividualEyeGaze = m_gazeTracker.AreLeftAndRightGazesSupported();

        // Get a spatial locator for the tracker. This will be used to transfer the gaze data to other coordinate systems later
        const auto trackerNodeId = m_gazeTracker.TrackerSpaceLocatorNodeId();
        m_trackerLocator = winrt::Windows::Perception::Spatial::Preview::SpatialGraphInteropPreview::CreateLocatorForNode(trackerNodeId);
   }
   catch (const winrt::hresult_error& e)
   {
       // Unable to open the tracker
   }
}

Настройка частоты кадров отслеживания взгляда

Свойство EyeGazeTracker.SupportedTargetFrameRates возвращает список целевой частоты кадров, поддерживаемой трекером. HoloLens 2 поддерживает 30, 60 и 90 кадров в секунду.

Используйте метод , EyeGazeTracker.SetTargetFrameRate() чтобы задать целевую частоту кадров.

// This returns a list of supported frame rate: 30, 60, 90 fps in order
var supportedFrameRates = _tracker.SupportedTargetFrameRates;

// Sets the tracker at the highest supported frame rate (90 fps)
var newFrameRate = supportedFrameRates[supportedFrameRates.Count - 1];
_tracker.SetTargetFrameRate(newFrameRate);
uint newFramesPerSecond = newFrameRate.FramesPerSecond;
// This returns a list of supported frame rate: 30, 60, 90 fps in order
const auto supportedFrameRates = m_gazeTracker.SupportedTargetFrameRates();

// Sets the tracker at the highest supported frame rate (90 fps)
const auto newFrameRate = supportedFrameRates.GetAt(supportedFrameRates.Size() - 1);
m_gazeTracker.SetTargetFrameRate(newFrameRate);
const uint32_t newFramesPerSecond = newFrameRate.FramesPerSecond();

Чтение данных о взгляде из средства отслеживания взгляда

Средство отслеживания взгляда периодически публикует свои состояния в круговом буфере. Это позволяет приложению считывать состояние средства отслеживания в момент, относящийся к небольшому временному интервалу. Он позволяет, например, получить последнее состояние средства отслеживания или его состояние во время какого-либо события, например жеста руки от пользователя.

Методы, извлекающие состояние средства отслеживания в виде экземпляра EyeGazeTrackerReading :

  • Методы TryGetReadingAtTimestamp() и TryGetReadingAtSystemRelativeTime() возвращают ближайший EyeGazeTrackerReading к времени, прошедшему приложением. Средство отслеживания управляет расписанием публикации, поэтому возвращаемое чтение может быть немного старше или новее времени запроса. Свойства EyeGazeTrackerReading.Timestamp и EyeGazeTrackerReading.SystemRelativeTime позволяют приложению узнать точное время опубликованного состояния.

  • Методы TryGetReadingAfterTimestamp() и TryGetReadingAfterSystemRelativeTime() возвращают первый EyeGazeTrackerReading с меткой времени, строго превосходящей время, прошедшее в качестве параметра. Это позволяет приложению последовательно считывать все состояния, опубликованные средствами отслеживания. Обратите внимание, что все эти методы запрашивают существующий буфер и возвращаются немедленно. Если состояние недоступно, они возвращают значение NULL (другими словами, они не заставят приложение ждать публикации состояния).

В дополнение к метке EyeGazeTrackerReading времени экземпляр имеет IsCalibrationValid свойство , которое указывает, допустима ли калибровка средства отслеживания взгляда.

Наконец, данные взгляда можно получить с помощью набора методов, таких как TryGetCombinedEyeGazeInTrackerSpace() или TryGetLeftEyeGazeInTrackerSpace(). Все эти методы возвращают логическое значение, указывающее на успешное выполнение. Сбой при получении некоторых данных может означать, что данные не поддерживаются (EyeGazeTracker имеет свойства для обнаружения этого случая) или что средство отслеживания не может получить данные (например, недопустимая калибровка или скрытие глаза).

Например, если приложение хочет отобразить курсор, соответствующий объединенному взгляду, оно может запросить средство отслеживания, используя метку времени прогноза подготавливаемого кадра, как показано ниже.

var holographicFrame = holographicSpace.CreateNextFrame();
var prediction = holographicFrame.CurrentPrediction;
var predictionTimestamp = prediction.Timestamp;
var reading = _tracker.TryGetReadingAtTimestamp(predictionTimestamp.TargetTime.DateTime);
if (reading != null)
{
    // Vector3 needs the System.Numerics namespace
    if (reading.TryGetCombinedEyeGazeInTrackerSpace(out Vector3 gazeOrigin, out Vector3 gazeDirection))
    {
        // Use gazeOrigin and gazeDirection to display the cursor
    }
}
auto holographicFrame = m_holographicSpace.CreateNextFrame();
auto prediction = holographicFrame.CurrentPrediction();
auto predictionTimestamp = prediction.Timestamp();
const auto reading = m_gazeTracker.TryGetReadingAtTimestamp(predictionTimestamp.TargetTime());
if (reading)
{
    float3 gazeOrigin;
    float3 gazeDirection;
    if (reading.TryGetCombinedEyeGazeInTrackerSpace(gazeOrigin, gazeDirection))
    {
        // Use gazeOrigin and gazeDirection to display the cursor
    }
}

Преобразование данных взгляда в другую SpatialCoordinateSystem

Для API-интерфейсов WinRT, возвращающих пространственные данные, такие как положение, всегда требуются как , PerceptionTimestamp так и SpatialCoordinateSystem. Например, чтобы получить объединенный взгляд HoloLens 2 с помощью API WinRT, API SpatialPointerPose.TryGetAtTimestamp() требует два параметра: SpatialCoordinateSystem и PerceptionTimestamp. При обращении к объединенному взгляду через SpatialPointerPose.Eyes.Gaze, его источник и направление выражаются в переданном объекте SpatialCoordinateSystem .

Api-интерфейсам пакета SDK для расширенного отслеживания tye не нужно принимать SpatialCoordinateSystem , а данные взгляда всегда выражаются в системе координат средства отслеживания. Но вы можете преобразовать эти данные взгляда в другую систему координат с помощью положения средства отслеживания, связанного с другой системой координат.

  • Как упоминалось в разделе "Открыть средство отслеживания взгляда" выше, чтобы получить SpatialLocator для средства отслеживания взгляда, вызовите Windows.Perception.Spatial.Preview.SpatialGraphInteropPreview.CreateLocatorForNode() с EyeGazeTracker.TrackerSpaceLocatorNodeId помощью свойства .

  • Источники и направления взгляда, полученные через EyeGazeTrackerReading , связаны с устройством отслеживания взгляда.

  • SpatialLocator.TryLocateAtTimestamp() возвращает полное расположение 6DoF средства отслеживания взгляда в заданном PerceptionTimeStamp объекте и связанном с заданным SpatialCoordinateSystemзначением , которое можно использовать для создания матрицы преобразования Matrix4x4.

  • Используйте матрицу преобразования Matrix4x4, созданную для передачи источников и направлений взгляда в другую spatialCoordinateSystem.

В следующих примерах кода показано, как вычислить положение куба, расположенного в направлении комбинированного взгляда, на два метра перед источником взгляда.

var predictionTimestamp = prediction.Timestamp;
var stationaryCS = stationaryReferenceFrame.CoordinateSystem;
var trackerLocation = _trackerLocator.TryLocateAtTimestamp(predictionTimestamp, stationaryCS);
if (trackerLocation != null)
{
    var trackerToStationaryMatrix = Matrix4x4.CreateFromQuaternion(trackerLocation.Orientation) * Matrix4x4.CreateTranslation(trackerLocation.Position);
    var reading = _tracker.TryGetReadingAtTimestamp(predictionTimestamp.TargetTime.DateTime);
    if (reading != null)
    {
        if (reading.TryGetCombinedEyeGazeInTrackerSpace(out Vector3 gazeOriginInTrackerSpace, out Vector3 gazeDirectionInTrackerSpace))
        {
            var cubePositionInTrackerSpace = gazeOriginInTrackerSpace + 2.0f * gazeDirectionInTrackerSpace;
            var cubePositionInStationaryCS = Vector3.Transform(cubePositionInTrackerSpace, trackerToStationaryMatrix);
        }
    }
}
auto predictionTimestamp = prediction.Timestamp();
auto stationaryCS = m_stationaryReferenceFrame.CoordinateSystem();
auto trackerLocation = m_trackerLocator.TryLocateAtTimestamp(predictionTimestamp, stationaryCS);
if (trackerLocation) 
{
    auto trackerOrientation = trackerLocation.Orientation();
    auto trackerPosition = trackerLocation.Position();
    auto trackerToStationaryMatrix = DirectX::XMMatrixRotationQuaternion(DirectX::XMLoadFloat4(reinterpret_cast<const DirectX::XMFLOAT4*>(&trackerOrientation))) * DirectX::XMMatrixTranslationFromVector(DirectX::XMLoadFloat3(&trackerPosition));

    const auto reading = m_gazeTracker.TryGetReadingAtTimestamp(predictionTimestamp.TargetTime());
    if (reading)
    {
        float3 gazeOriginInTrackerSpace;
        float3 gazeDirectionInTrackerSpace;
        if (reading.TryGetCombinedEyeGazeInTrackerSpace(gazeOriginInTrackerSpace, gazeDirectionInTrackerSpace))
        {
            auto cubePositionInTrackerSpace = gazeOriginInTrackerSpace + 2.0f * gazeDirectionInTrackerSpace;
            float3 cubePositionInStationaryCS;
            DirectX::XMStoreFloat3(&cubePositionInStationaryCS, DirectX::XMVector3TransformCoord(DirectX::XMLoadFloat3(&cubePositionInTrackerSpace), trackerToStationaryMatrix));
        }
    }
}

Справочник по API расширенного пакета SDK для отслеживания глаз

namespace Microsoft.MixedReality.EyeTracking
{
    /// <summary>
    /// Allow discovery of Eye Gaze Trackers connected to the system
    /// This is the only class from Extended Eye Tracking SDK that the application will instantiate, 
    /// other classes' instances will be returned by method calls or properties.
    /// </summary>
    public class EyeGazeTrackerWatcher
    {
        /// <summary>
        /// Constructs an instance of the watcher
        /// </summary>
        public EyeGazeTrackerWatcher();

        /// <summary>
        /// Starts trackers enumeration.
        /// </summary>
        /// <returns>Task representing async action; completes when the initial enumeration is completed</returns>
        public System.Threading.Tasks.Task StartAsync();

        /// <summary>
        /// Stop listening to trackers additions and removal
        /// </summary>
        public void Stop();

        /// <summary>
        /// Raised when an Eye Gaze tracker is connected
        /// </summary>
        public event System.EventHandler<EyeGazeTracker> EyeGazeTrackerAdded;

        /// <summary>
        /// Raised when an Eye Gaze tracker is disconnected
        /// </summary>
        public event System.EventHandler<EyeGazeTracker> EyeGazeTrackerRemoved;        
    }

    /// <summary>
    /// Represents an Eye Tracker device
    /// </summary>
    public class EyeGazeTracker
    {
        /// <summary>
        /// True if Restricted mode is supported, which means the driver supports to provide individual 
        /// eye gaze vector and framerate 
        /// </summary>
        public bool IsRestrictedModeSupported;

        /// <summary>
        /// True if Vergence Distance is supported by tracker
        /// </summary>
        public bool IsVergenceDistanceSupported;

        /// <summary>
        /// True if Eye Openness is supported by the driver
        /// </summary>
        public bool IsEyeOpennessSupported;

        /// <summary>
        /// True if individual gazes are supported
        /// </summary>
        public bool AreLeftAndRightGazesSupported;

        /// <summary>
        /// Get the supported target frame rates of the tracker
        /// </summary>
        public System.Collections.Generic.IReadOnlyList<EyeGazeTrackerFrameRate> SupportedTargetFrameRates;

        /// <summary>
        /// NodeId of the tracker, used to retrieve a SpatialLocator or SpatialGraphNode to locate the tracker in the scene
        /// for Perception API, use SpatialGraphInteropPreview.CreateLocatorForNode
        /// for Mixed Reality OpenXR API, use SpatialGraphNode.FromDynamicNodeId
        /// </summary>
        public Guid TrackerSpaceLocatorNodeId;

        /// <summary>
        /// Opens the tracker
        /// </summary>
        /// <param name="restrictedMode">True if restricted mode active</param>
        /// <returns>Task representing async action; completes when the initial enumeration is completed</returns>
        public System.Threading.Tasks.Task OpenAsync(bool restrictedMode);

        /// <summary>
        /// Closes the tracker
        /// </summary>
        public void Close();

        /// <summary>
        /// Changes the target frame rate of the tracker
        /// </summary>
        /// <param name="newFrameRate">Target frame rate</param>
        public void SetTargetFrameRate(EyeGazeTrackerFrameRate newFrameRate);

        /// <summary>
        /// Try to get tracker state at a given timestamp
        /// </summary>
        /// <param name="timestamp">timestamp</param>
        /// <returns>State if available, null otherwise</returns>
        public EyeGazeTrackerReading TryGetReadingAtTimestamp(DateTime timestamp);

        /// <summary>
        /// Try to get tracker state at a system relative time
        /// </summary>
        /// <param name="time">time</param>
        /// <returns>State if available, null otherwise</returns>
        public EyeGazeTrackerReading TryGetReadingAtSystemRelativeTime(TimeSpan time);

        /// <summary>
        /// Try to get first first tracker state after a given timestamp
        /// </summary>
        /// <param name="timestamp">timestamp</param>
        /// <returns>State if available, null otherwise</returns>
        public EyeGazeTrackerReading TryGetReadingAfterTimestamp(DateTime timestamp);

        /// <summary>
        /// Try to get the first tracker state after a system relative time
        /// </summary>
        /// <param name="time">time</param>
        /// <returns>State if available, null otherwise</returns>
        public EyeGazeTrackerReading TryGetReadingAfterSystemRelativeTime(TimeSpan time);
    }

    /// <summary>
    /// Represents a Frame Rate supported by an Eye Tracker
    /// </summary>
    public class EyeGazeTrackerFrameRate
    {
        /// <summary>
        /// Frames per second of the frame rate
        /// </summary>
        public UInt32 FramesPerSecond;
    }

    /// <summary>
    /// Snapshot of Gaze Tracker state
    /// </summary>
    public class EyeGazeTrackerReading
    {
        /// <summary>
        /// Timestamp of state
        /// </summary>
        public DateTime Timestamp;

        /// <summary>
        /// Timestamp of state as system relative time
        /// Its SystemRelativeTime.Ticks could provide the QPC time to locate tracker pose 
        /// </summary>
        public TimeSpan SystemRelativeTime;

        /// <summary>
        /// Indicates user calibration is valid
        /// </summary>
        public bool IsCalibrationValid;

        /// <summary>
        /// Tries to get a vector representing the combined gaze related to the tracker's node
        /// </summary>
        /// <param name="origin">Origin of the gaze vector</param>
        /// <param name="direction">Direction of the gaze vector</param>
        /// <returns></returns>
        public bool TryGetCombinedEyeGazeInTrackerSpace(out System.Numerics.Vector3 origin, out System.Numerics.Vector3 direction);

        /// <summary>
        /// Tries to get a vector representing the left eye gaze related to the tracker's node
        /// </summary>
        /// <param name="origin">Origin of the gaze vector</param>
        /// <param name="direction">Direction of the gaze vector</param>
        /// <returns></returns>
        public bool TryGetLeftEyeGazeInTrackerSpace(out System.Numerics.Vector3 origin, out System.Numerics.Vector3 direction);

        /// <summary>
        /// Tries to get a vector representing the right eye gaze related to the tracker's node position
        /// </summary>
        /// <param name="origin">Origin of the gaze vector</param>
        /// <param name="direction">Direction of the gaze vector</param>
        /// <returns></returns>
        public bool TryGetRightEyeGazeInTrackerSpace(out System.Numerics.Vector3 origin, out System.Numerics.Vector3 direction);

        /// <summary>
        /// Tries to read vergence distance
        /// </summary>
        /// <param name="value">Vergence distance if available</param>
        /// <returns>bool if value is valid</returns>
        public bool TryGetVergenceDistance(out float value);

        /// <summary>
        /// Tries to get left Eye openness information
        /// </summary>
        /// <param name="value">Eye Openness if valid</param>
        /// <returns>bool if value is valid</returns>
        public bool TryGetLeftEyeOpenness(out float value);

        /// <summary>
        /// Tries to get right Eye openness information
        /// </summary>
        /// <param name="value">Eye Openness if valid</param>
        /// <returns>bool if value is valid</returns>
        public bool TryGetRightEyeOpenness(out float value);
    }
}

См. также раздел