Controlo de olhos expandido no motor nativo
O controlo ocular alargado é uma nova capacidade no HoloLens 2. É um superconjunto do controlo ocular padrão, que apenas fornece dados combinados de olhar para os olhos. O controlo de olhos alargado também fornece dados de olhar individual e permite que as aplicações definam diferentes taxas de fotogramas para os dados de olhar, como 30, 60 e 90fps. Outras funcionalidades, como a abertura ocular e a vergência ocular, não são suportadas por HoloLens 2 neste momento.
O SDK de Controlo Ocular Alargado permite que as aplicações acedam a dados e funcionalidades de controlo ocular alargado. Pode ser utilizado em conjunto com APIs WinRT ou APIs OpenXR.
Este artigo aborda as formas de utilizar o SDK de controlo ocular expandido no motor nativo (C# ou C+++/WinRT), juntamente com as APIs WinRT.
Configuração do projeto
-
Crie um
Holographic DirectX 11 App (Universal Windows)
projeto ouHolographic DirectX 11 App (Universal Windows) (C++/WinRT)
com o Visual Studio 2019 ou mais recente ou abra o projeto holográfico existente do Visual Studio. - Importe o SDK de controlo ocular expandido para o projeto.
- No Explorador de Soluções do Visual Studio, clique com o botão direito do rato no projeto –> Gerir Pacotes NuGet...
- Certifique-se de que a origem do pacote no canto superior direito aponta para nuget.org: https://api.nuget.org/v3/index.json
- Clique no separador Browser e, em seguida, procure
Microsoft.MixedReality.EyeTracking
. - Clique no botão Instalar para instalar a versão mais recente do SDK.
- Definir a capacidade de Entrada de Olhar
- Faça duplo clique no ficheiro Package.appxmanifest no Explorador de Soluções.
- Clique no separador Capacidades e, em seguida, selecione a Entrada de Olhar.
- Inclua o ficheiro principal e utilize o espaço de nomes.
- 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;
- Consuma as APIs do SDK de controlo ocular alargado e implemente a sua lógica.
- Criar e implementar no HoloLens.
Descrição geral dos passos para obter os dados de olhar
Obter os dados de olhar para os olhos através das APIs do SDK de Controlo Ocular Alargado requer os seguintes passos:
- Obtenha acesso às funcionalidades de Controlo ocular do utilizador.
- Tenha em atenção as ligações e as desconexões do controlador de olhar para os olhos.
- Abra o controlador de olhar para os olhos e, em seguida, consulte as suas capacidades.
- Ler repetidamente dados de olhar do controlador de olhar para os olhos.
- Transfira dados de olhar para outros SpatialCoordinateSystems.
Obter acesso às funcionalidades de controlo ocular
Para utilizar quaisquer informações relacionadas com os olhos, a aplicação tem primeiro de pedir o consentimento do utilizador.
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);
Detetar controlador de olhar para os olhos
A deteção do controlador de olhar para os olhos é feita através da utilização da EyeGazeTrackerWatcher
classe.
EyeGazeTrackerAdded
e EyeGazeTrackerRemoved
os eventos são, respectivamente, gerados quando um controlador de olhar para os olhos é detetado ou desligado.
O observador tem de ser explicitamente iniciado com o StartAsync()
método, que é concluído de forma assíncrona quando os controladores que já estão ligados foram sinalizados através do EyeGazeTrackerAdded
evento.
Quando é detetado um controlador de olhar para os olhos, é transmitida uma EyeGazeTracker
instância para a aplicação nos parâmetros do EyeGazeTrackerAdded
evento; reciprocamente, quando um controlador é desligado, a instância correspondente EyeGazeTracker
é transmitida 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 controlador de olhar para os olhos
Ao receber uma EyeGazeTracker
instância, a aplicação tem primeiro de a abrir ao chamar o OpenAsync()
método. Em seguida, pode consultar as capacidades do controlador, se necessário. O OpenAsync()
método utiliza um parâmetro booleano; isto indica se a aplicação precisa de aceder a funcionalidades que não pertencem ao controlo ocular padrão, como vetores individuais de olhar para os olhos ou alterar a taxa de fotogramas do controlador.
O olhar combinado é uma funcionalidade obrigatória suportada por todos os controladores de olhar para os olhos. Outras funcionalidades, como o acesso ao olhar individual, são opcionais e podem ser suportadas ou não consoante o controlador e o controlador. Para estas funcionalidades opcionais, a EyeGazeTracker
classe expõe uma propriedade que indica se a funcionalidade é suportada, por exemplo, a AreLeftAndRightGazesSupported
propriedade, que indica se as informações individuais de olhar para os olhos são suportadas pelo dispositivo.
Todas as informações espaciais expostas pelo controlador de olhar para os olhos são publicadas relacionadas com um controlador em si, que é identificado por um ID de Nó Dinâmico. Utilizar o nodeId para obter uma SpatialCoordinateSystem
com APIs WinRT pode transformar as coordenadas dos dados de olhar para 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 a taxa de fotogramas do controlador de olhar para os olhos
A EyeGazeTracker.SupportedTargetFrameRates
propriedade devolve a lista da taxa de fotogramas de destino suportada pelo controlador. HoloLens 2 suporta 30, 60 e 90fps.
Utilize o EyeGazeTracker.SetTargetFrameRate()
método para definir a taxa de fotogramas 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 olhar do controlador de olhar para os olhos
Um controlador de olhar para os olhos publica periodicamente os seus estados numa memória intermédia circular. Isto permite que a aplicação leia o estado do controlador de cada vez que pertence a um intervalo de tempo pequeno. Permite, por exemplo, a obtenção do estado mais recente do controlador ou o respetivo estado no momento de algum evento, como um gesto de mão do utilizador.
Métodos que obtêm o estado do controlador como uma EyeGazeTrackerReading
instância:
Os
TryGetReadingAtTimestamp()
métodos eTryGetReadingAtSystemRelativeTime()
devolvem oEyeGazeTrackerReading
mais próximo do tempo passado pela aplicação. O controlador controla a agenda de publicação, pelo que a leitura devolvida pode ser ligeiramente mais antiga ou mais recente do que a hora do pedido. AsEyeGazeTrackerReading.Timestamp
propriedades eEyeGazeTrackerReading.SystemRelativeTime
permitem que a aplicação saiba a hora exata do estado publicado.Os
TryGetReadingAfterTimestamp()
métodos eTryGetReadingAfterSystemRelativeTime()
devolvem o primeiroEyeGazeTrackerReading
com um carimbo de data/hora estritamente superior ao tempo passado como parâmetro. Isto permite que uma aplicação leia sequencialmente todos os estados publicados pelo controlador. Tenha em atenção que todos estes métodos estão a consultar a memória intermédia existente e que regressam imediatamente. Se não existir nenhum estado disponível, devolverão nulos (ou seja, não farão com que a aplicação aguarde pela 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 controlador ocular é válida ou não.
Por fim, os dados de olhar podem ser obtidos através de um conjunto de métodos como TryGetCombinedEyeGazeInTrackerSpace()
ou TryGetLeftEyeGazeInTrackerSpace()
. Todos estes métodos devolvem um valor booleano que indica um êxito. A não obtenção de alguns dados pode significar que os dados não são suportados (EyeGazeTracker
tem propriedades para detetar este caso) ou que o controlador não conseguiu obter os dados (por exemplo, calibragem inválida ou ocultação ocular).
Se, por exemplo, a aplicação quiser apresentar um cursor correspondente ao olhar combinado, pode consultar o controlador com um carimbo de data/hora da predição da moldura que está a ser preparada da seguinte forma.
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 olhar para outro SpatialCoordinateSystem
As APIs WinRT que devolvem dados espaciais, como uma posição, requerem sempre um PerceptionTimestamp
e um SpatialCoordinateSystem
. Por exemplo, para obter o olhar combinado de HoloLens 2 com a API WinRT, a API SpatialPointerPose.TryGetAtTimestamp() requer dois parâmetros: a SpatialCoordinateSystem
e um PerceptionTimestamp
. Quando o olhar combinado é então acedido através SpatialPointerPose.Eyes.Gaze
de , a sua origem e direção são expressas na SpatialCoordinateSystem
passagem.
As APIs do SDK de controlo de tye alargadas não precisam de utilizar e SpatialCoordinateSystem
os dados de olhar são sempre expressos no sistema de coordenadas do controlador. Mas pode transformar esses dados de olhar para outro sistema de coordenadas com a pose do controlador relacionada com o outro sistema de coordenadas.
Como a secção acima chamada "Open eye gaze tracker" mencionou, para obter um
SpatialLocator
para o controlador de olhar para os olhos, ligueWindows.Perception.Spatial.Preview.SpatialGraphInteropPreview.CreateLocatorForNode()
com aEyeGazeTracker.TrackerSpaceLocatorNodeId
propriedade.As origens de olhar e as direções obtidas estão
EyeGazeTrackerReading
relacionadas com o controlador de olhar para os olhos.SpatialLocator.TryLocateAtTimestamp()
devolve a localização 6DoF completa do controlador de olhar para os olhos num determinadoPerceptionTimeStamp
e relacionado com um determinadoSpatialCoordinateSystem
, que poderia ser utilizado para construir uma matriz de transformação Matrix4x4.Utilize a matriz de transformação Matrix4x4 construída para transferir as origens e direções do olhar para outro SpatialCoordinateSystem.
Os exemplos de código seguintes mostram como calcular a posição de um cubo localizado na direção do olhar combinado, dois metros à frente da origem do olhar;
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 da API do SDK de controlo ocular alargado
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);
}
}