Compartilhar via


Acompanhamento ocular estendido no mecanismo nativo

O acompanhamento ocular estendido é uma nova funcionalidade em HoloLens 2. É um superconjunto do acompanhamento visual padrão, que fornece apenas dados combinados de foco ocular. O acompanhamento ocular estendido também fornece dados individuais de foco ocular e permite que os aplicativos definam diferentes taxas de quadros para os dados de foco, como 30, 60 e 90fps. Não há suporte para outros recursos, como abertura ocular e vergence ocular, HoloLens 2 no momento.

O SDK de Acompanhamento Ocular Estendido permite que os aplicativos acessem dados e recursos de acompanhamento ocular estendido. Ele pode ser usado junto com APIs do WinRT ou APIs OpenXR.

Este artigo aborda as maneiras de usar o SDK de acompanhamento ocular estendido no mecanismo nativo (C# ou C++/WinRT), juntamente com APIs do WinRT.

Configuração do projeto

  1. Crie um Holographic DirectX 11 App (Universal Windows) projeto ou Holographic DirectX 11 App (Universal Windows) (C++/WinRT) com o Visual Studio 2019 ou mais recente ou abra seu projeto holográfico do Visual Studio existente.
  2. Importe o SDK de acompanhamento ocular estendido para o projeto.
    1. No Gerenciador de Soluções do Visual Studio, clique com o botão direito do mouse em seu projeto –> Gerenciar Pacotes NuGet...
    2. Verifique se a origem do pacote no canto superior direito aponta para nuget.org: https://api.nuget.org/v3/index.json
    3. Clique na guia Navegador e pesquise Microsoft.MixedReality.EyeTracking.
    4. Clique no botão Instalar para instalar a versão mais recente do SDK.
      Captura de tela do pacote Nuget do SDK de Acompanhamento Ocular.
  3. Definir a funcionalidade de Entrada de Foco
    1. Clique duas vezes no arquivo Package.appxmanifest em Gerenciador de Soluções.
    2. Clique na guia Funcionalidades e, em seguida, marcar a Entrada de Foco.
  4. Inclua o arquivo principal e use o namespace.
    • Para um projeto C#:
    using Microsoft.MixedReality.EyeTracking;
    
    • Para um projeto C++/WinRT:
    #include <winrt/Microsoft.MixedReality.EyeTracking.h>
    using namespace winrt::Microsoft::MixedReality::EyeTracking;
    
  5. Consuma as APIs do SDK de acompanhamento ocular estendido e implemente sua lógica.
  6. Compilar e implantar no HoloLens.

Visão geral das etapas para obter os dados de foco

Obter os dados de foco visual por meio das APIs do SDK de Acompanhamento Ocular Estendido requer as seguintes etapas:

  1. Obtenha acesso aos recursos de Rastreamento ocular do usuário.
  2. Observe as conexões e desconexões do rastreador de olhar.
  3. Abra o rastreador de olhar e, em seguida, consulte suas funcionalidades.
  4. Leia repetidamente os dados de foco do rastreador de olhar.
  5. Transferir dados de foco para outros SpatialCoordinateSystems.

Obter acesso aos recursos de acompanhamento ocular

Para usar qualquer informação relacionada aos olhos, o aplicativo deve primeiro solicitar o consentimento do usuário.

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);

Detectar rastreador de foco ocular

A detecção do rastreador de olhar é feita por meio do uso da EyeGazeTrackerWatcher classe . EyeGazeTrackerAdded e EyeGazeTrackerRemoved os eventos são gerados respectivamente quando um rastreador de foco ocular é detectado ou desconectado.

O observador deve ser iniciado explicitamente com o StartAsync() método , que é concluído de forma assíncrona quando os rastreadores que já estão conectados foram sinalizados por meio do EyeGazeTrackerAdded evento.

Quando um rastreador de foco ocular é detectado, uma EyeGazeTracker instância é passada para o aplicativo nos parâmetros de EyeGazeTrackerAdded evento; de forma recíproca, quando um rastreador é desconectado, a instância correspondente EyeGazeTracker é passada para o evento 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)
{
    ...
}

Abrir o rastreador de olhar

Ao receber uma EyeGazeTracker instância, o aplicativo deve primeiro abri-la chamando o OpenAsync() método . Em seguida, ele pode consultar os recursos do rastreador, se necessário. O OpenAsync() método usa um parâmetro booliano; isso indica se o aplicativo precisa acessar recursos que não pertencem ao rastreamento ocular padrão, como vetores individuais de foco ocular ou alteração da taxa de quadros do rastreador.

O foco combinado é um recurso obrigatório com suporte de todos os rastreadores de foco ocular. Outros recursos, como o acesso ao foco individual, são opcionais e podem ter suporte ou não dependendo do rastreador e do driver. Para esses recursos opcionais, a EyeGazeTracker classe expõe uma propriedade que indica se há suporte para o recurso , por exemplo, a AreLeftAndRightGazesSupported propriedade , que indica se as informações individuais de foco ocular têm suporte do dispositivo.

Todas as informações espaciais expostas pelo rastreador de foco visual são publicadas relacionadas a um rastreador em si, que é identificado por uma ID de Nó Dinâmico. Usar o nodeId para obter um SpatialCoordinateSystem com APIs winRT pode transformar as coordenadas dos dados de foco em outro sistema de coordenadas.

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
   }
}

Definir taxa de quadros do rastreador de foco ocular

A EyeGazeTracker.SupportedTargetFrameRates propriedade retorna a lista da taxa de quadros de destino com suporte do rastreador. HoloLens 2 dá suporte a 30, 60 e 90fps.

Use o EyeGazeTracker.SetTargetFrameRate() método para definir a taxa de quadros de destino.

// 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();

Ler dados de foco do rastreador de olhar

Um rastreador de olhar publica seus estados periodicamente em um buffer circular. Isso permite que o aplicativo leia o estado do rastreador em um período de tempo pequeno. Ele permite, por exemplo, a recuperação do estado mais recente do rastreador ou seu estado no momento de algum evento, como um gesto de mão do usuário.

Métodos que recuperam o estado do rastreador como uma EyeGazeTrackerReading instância:

  • Os TryGetReadingAtTimestamp() métodos e TryGetReadingAtSystemRelativeTime() retornam o EyeGazeTrackerReading mais próximo do tempo passado pelo aplicativo. O rastreador controla o agendamento de publicação, portanto, a leitura retornada pode ser um pouco mais antiga ou mais recente do que o tempo de solicitação. As EyeGazeTrackerReading.Timestamp propriedades e EyeGazeTrackerReading.SystemRelativeTime permitem que o aplicativo saiba a hora exata do estado publicado.

  • Os TryGetReadingAfterTimestamp() métodos e TryGetReadingAfterSystemRelativeTime() retornam o primeiro EyeGazeTrackerReading com um carimbo de data/hora estritamente superior ao tempo passado como parâmetro. Isso permite que um aplicativo leia sequencialmente todos os estados publicados pelo rastreador. Observe que todos esses métodos estão consultando o buffer existente e que eles retornam imediatamente. Se nenhum estado estiver disponível, eles retornarão nulo (em outras palavras, não farão com que o aplicativo aguarde a publicação de um estado).

Além do carimbo de data/hora, uma EyeGazeTrackerReading instância tem uma IsCalibrationValid propriedade , que indica se a calibragem do rastreador ocular é válida ou não.

Por fim, os dados de foco podem ser recuperados por meio de um conjunto de métodos como TryGetCombinedEyeGazeInTrackerSpace() ou TryGetLeftEyeGazeInTrackerSpace(). Todos esses métodos retornam um booliano indicando um sucesso. A falha ao obter alguns dados pode significar que os dados não têm suporte (EyeGazeTracker tem propriedades para detectar esse caso) ou que o rastreador não pôde obter os dados (por exemplo, calibragem inválida ou olho oculto).

Se, por exemplo, o aplicativo quiser exibir um cursor correspondente ao foco combinado, ele poderá consultar o rastreador usando um carimbo de data/hora da previsão do quadro que está sendo preparado da seguinte maneira.

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
    }
}

Transformar dados de foco em outros SpatialCoordinateSystem

As APIs do WinRT que retornam dados espaciais, como uma posição, sempre exigem um PerceptionTimestamp e um SpatialCoordinateSystem. Por exemplo, para recuperar o foco combinado de HoloLens 2 usando a API WinRT, a API SpatialPointerPose.TryGetAtTimestamp() requer dois parâmetros: a SpatialCoordinateSystem e um PerceptionTimestamp. Quando o foco combinado é então acessado por meio SpatialPointerPose.Eyes.Gazede , sua origem e direção são expressas no SpatialCoordinateSystem passado.

As APIs do SDK de acompanhamento de tye estendidas não precisam usar um SpatialCoordinateSystem e os dados de foco sempre são expressos no sistema de coordenadas do rastreador. Mas você pode transformar esses dados de foco em outro sistema de coordenadas com a pose do rastreador relacionada ao outro sistema de coordenadas.

  • Como a seção acima denominada "Abrir rastreador de foco ocular" mencionada, para obter um SpatialLocator para o rastreador de olhar, chame Windows.Perception.Spatial.Preview.SpatialGraphInteropPreview.CreateLocatorForNode() com a EyeGazeTracker.TrackerSpaceLocatorNodeId propriedade .

  • As origens de foco e as direções recuperadas por meio EyeGazeTrackerReading estão relacionadas ao rastreador de foco ocular.

  • SpatialLocator.TryLocateAtTimestamp() retorna o local 6DoF completo do rastreador de foco ocular em um determinado PerceptionTimeStamp e relacionado a um determinado SpatialCoordinateSystem, que poderia ser usado para construir uma matriz de transformação Matrix4x4.

  • Use a matriz de transformação Matrix4x4 construída para transferir as origens de foco e as direções para outro SpatialCoordinateSystem.

Os exemplos de código a seguir mostram como calcular a posição de um cubo localizado na direção do foco combinado, dois metros na frente da origem do foco;

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));
        }
    }
}

Referência de API do SDK de acompanhamento ocular estendido

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);
    }
}

Confira também