Interacciones de mirada y seguimiento ocular en aplicaciones de Windows
Proporcionar compatibilidad con el seguimiento de la mirada, la atención y la presencia de un usuario en función de la ubicación y el movimiento de sus ojos.
Nota:
Para ver la entrada de mirada en Windows Mixed Reality, consulta [Gaze]/windows/mixed-reality/mrtk-unity/features/input/gaze).
API importantes: Windows.Devices.Input.Preview, GazeDevicePreview, GazePointPreview, GazeInputSourcePreview
Información general
La entrada de mirada es una forma eficaz de interactuar y usar aplicaciones de Windows que es especialmente útil como tecnología de asistencia para los usuarios con enfermedades neuro-musculares (como el ALS) y otras discapacidades que implican funciones musculares o nerviosas deficientes.
Además, la entrada de mirada ofrece oportunidades igualmente atractivas para los juegos (incluida la adquisición de destino y el seguimiento) y las aplicaciones tradicionales de productividad, quioscos y otros escenarios interactivos en los que los dispositivos de entrada tradicionales (teclado, mouse, táctil) no están disponibles o donde puede ser útil/útil liberar las manos del usuario para otras tareas (como mantener las bolsas de compras).
Nota:
La compatibilidad con el hardware de seguimiento ocular se introdujo en Windows 10 Fall Creators Update junto con el control Eye, una característica integrada que permite usar los ojos para controlar el puntero en pantalla, escribir con el teclado en pantalla y comunicarse con personas que usan texto a voz. Un conjunto de API de Windows Runtime (Windows.Devices.Input.Preview) para compilar aplicaciones que pueden interactuar con el hardware de seguimiento ocular está disponible con la actualización de abril de 2018 de Windows 10 (versión 1803, compilación 17134) y versiones más recientes.
Privacidad
Debido a los datos personales potencialmente confidenciales recopilados por los dispositivos de seguimiento ocular, es necesario declarar la gazeInput
funcionalidad en el manifiesto de la aplicación de la aplicación (consulte la sección configuración siguiente). Cuando se declara, Windows solicita automáticamente a los usuarios un cuadro de diálogo de consentimiento (cuando la aplicación se ejecuta por primera vez), donde el usuario debe conceder permiso para que la aplicación se comunique con el dispositivo de seguimiento ocular y acceda a estos datos.
Además, si la aplicación recopila, almacena o transfiere datos de seguimiento ocular, debes describirlo en la declaración de privacidad de la aplicación y seguir todos los demás requisitos de información personal en el Contrato para desarrolladores de aplicaciones y las directivas de Microsoft Store.
Configurar
Para usar las API de entrada de mirada en la aplicación de Windows, deberá hacer lo siguiente:
Especifique la
gazeInput
funcionalidad en el manifiesto de la aplicación.Abra el archivo Package.appxmanifest con el diseñador de manifiestos de Visual Studio o agregue la funcionalidad manualmente seleccionando Ver código e insertando lo siguiente
DeviceCapability
en elCapabilities
nodo:<Capabilities> <DeviceCapability Name="gazeInput" /> </Capabilities>
Un dispositivo de seguimiento ocular compatible con Windows conectado al sistema (integrado o periférico) y activado.
Consulta Introducción al control ocular en Windows 10 para obtener una lista de dispositivos de seguimiento ocular compatibles.
Seguimiento ocular básico
En este ejemplo, se muestra cómo realizar un seguimiento de la mirada del usuario dentro de una aplicación de Windows y usar una función de control de tiempo con pruebas de posicionamiento básicas para indicar qué tan bien puede mantener el foco de mirada en un elemento específico.
Se usa una pequeña elipse para mostrar dónde se encuentra el punto de mirada dentro de la ventanilla de la aplicación, mientras que radialProgressBar del kit de herramientas de la comunidad de Windows se coloca aleatoriamente en el lienzo. Cuando se detecta el foco de mirada en la barra de progreso, se inicia un temporizador y la barra de progreso se reubica aleatoriamente en el lienzo cuando la barra de progreso alcanza el 100 %.
Seguimiento de miradas con ejemplo de temporizador
Descargue este ejemplo del ejemplo de entrada de Mirada (básico)
En primer lugar, configuramos la interfaz de usuario (MainPage.xaml).
<Page x:Class="gazeinput.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:gazeinput" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls" mc:Ignorable="d"> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid x:Name="containerGrid"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel x:Name="HeaderPanel" Orientation="Horizontal" Grid.Row="0"> <StackPanel.Transitions> <TransitionCollection> <AddDeleteThemeTransition/> </TransitionCollection> </StackPanel.Transitions> <TextBlock x:Name="Header" Text="Gaze tracking sample" Style="{ThemeResource HeaderTextBlockStyle}" Margin="10,0,0,0" /> <TextBlock x:Name="TrackerCounterLabel" VerticalAlignment="Center" Style="{ThemeResource BodyTextBlockStyle}" Text="Number of trackers: " Margin="50,0,0,0"/> <TextBlock x:Name="TrackerCounter" VerticalAlignment="Center" Style="{ThemeResource BodyTextBlockStyle}" Text="0" Margin="10,0,0,0"/> <TextBlock x:Name="TrackerStateLabel" VerticalAlignment="Center" Style="{ThemeResource BodyTextBlockStyle}" Text="State: " Margin="50,0,0,0"/> <TextBlock x:Name="TrackerState" VerticalAlignment="Center" Style="{ThemeResource BodyTextBlockStyle}" Text="n/a" Margin="10,0,0,0"/> </StackPanel> <Canvas x:Name="gazePositionCanvas" Grid.Row="1"> <controls:RadialProgressBar x:Name="GazeRadialProgressBar" Value="0" Foreground="Blue" Background="White" Thickness="4" Minimum="0" Maximum="100" Width="100" Height="100" Outline="Gray" Visibility="Collapsed"/> <Ellipse x:Name="eyeGazePositionEllipse" Width="20" Height="20" Fill="Blue" Opacity="0.5" Visibility="Collapsed"> </Ellipse> </Canvas> </Grid> </Grid> </Page>
A continuación, inicializamos nuestra aplicación.
En este fragmento de código, declaramos nuestros objetos globales e invalidamos el evento de página OnNavigatedTo para iniciar nuestro monitor de dispositivo de mirada y el evento de página OnNavigatedFrom para detener nuestro monitor de dispositivo de mirada.
using System; using Windows.Devices.Input.Preview; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml; using Windows.Foundation; using System.Collections.Generic; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Navigation; namespace gazeinput { public sealed partial class MainPage : Page { /// <summary> /// Reference to the user's eyes and head as detected /// by the eye-tracking device. /// </summary> private GazeInputSourcePreview gazeInputSource; /// <summary> /// Dynamic store of eye-tracking devices. /// </summary> /// <remarks> /// Receives event notifications when a device is added, removed, /// or updated after the initial enumeration. /// </remarks> private GazeDeviceWatcherPreview gazeDeviceWatcher; /// <summary> /// Eye-tracking device counter. /// </summary> private int deviceCounter = 0; /// <summary> /// Timer for gaze focus on RadialProgressBar. /// </summary> DispatcherTimer timerGaze = new DispatcherTimer(); /// <summary> /// Tracker used to prevent gaze timer restarts. /// </summary> bool timerStarted = false; /// <summary> /// Initialize the app. /// </summary> public MainPage() { InitializeComponent(); } /// <summary> /// Override of OnNavigatedTo page event starts GazeDeviceWatcher. /// </summary> /// <param name="e">Event args for the NavigatedTo event</param> protected override void OnNavigatedTo(NavigationEventArgs e) { // Start listening for device events on navigation to eye-tracking page. StartGazeDeviceWatcher(); } /// <summary> /// Override of OnNavigatedFrom page event stops GazeDeviceWatcher. /// </summary> /// <param name="e">Event args for the NavigatedFrom event</param> protected override void OnNavigatedFrom(NavigationEventArgs e) { // Stop listening for device events on navigation from eye-tracking page. StopGazeDeviceWatcher(); } } }
A continuación, agregamos nuestros métodos de monitor de dispositivo de mirada.
En
StartGazeDeviceWatcher
, llamamos a CreateWatcher y declaramos los agentes de escucha de eventos del dispositivo de seguimiento ocular (DeviceAdded, DeviceUpdated y DeviceRemoved).En
DeviceAdded
, comprobamos el estado del dispositivo de seguimiento ocular. Si es un dispositivo viable, incrementamos el número de dispositivos y habilitamos el seguimiento de miradas. Consulte el paso siguiente para obtener más información.En
DeviceUpdated
, también se habilita el seguimiento de miradas, ya que este evento se desencadena si se vuelve a calibrar un dispositivo.En
DeviceRemoved
, disminuyemos el contador del dispositivo y quitamos los controladores de eventos del dispositivo.En
StopGazeDeviceWatcher
, apagamos el monitor del dispositivo de mirada.
/// <summary>
/// Start gaze watcher and declare watcher event handlers.
/// </summary>
private void StartGazeDeviceWatcher()
{
if (gazeDeviceWatcher == null)
{
gazeDeviceWatcher = GazeInputSourcePreview.CreateWatcher();
gazeDeviceWatcher.Added += this.DeviceAdded;
gazeDeviceWatcher.Updated += this.DeviceUpdated;
gazeDeviceWatcher.Removed += this.DeviceRemoved;
gazeDeviceWatcher.Start();
}
}
/// <summary>
/// Shut down gaze watcher and stop listening for events.
/// </summary>
private void StopGazeDeviceWatcher()
{
if (gazeDeviceWatcher != null)
{
gazeDeviceWatcher.Stop();
gazeDeviceWatcher.Added -= this.DeviceAdded;
gazeDeviceWatcher.Updated -= this.DeviceUpdated;
gazeDeviceWatcher.Removed -= this.DeviceRemoved;
gazeDeviceWatcher = null;
}
}
/// <summary>
/// Eye-tracking device connected (added, or available when watcher is initialized).
/// </summary>
/// <param name="sender">Source of the device added event</param>
/// <param name="e">Event args for the device added event</param>
private void DeviceAdded(GazeDeviceWatcherPreview source,
GazeDeviceWatcherAddedPreviewEventArgs args)
{
if (IsSupportedDevice(args.Device))
{
deviceCounter++;
TrackerCounter.Text = deviceCounter.ToString();
}
// Set up gaze tracking.
TryEnableGazeTrackingAsync(args.Device);
}
/// <summary>
/// Initial device state might be uncalibrated,
/// but device was subsequently calibrated.
/// </summary>
/// <param name="sender">Source of the device updated event</param>
/// <param name="e">Event args for the device updated event</param>
private void DeviceUpdated(GazeDeviceWatcherPreview source,
GazeDeviceWatcherUpdatedPreviewEventArgs args)
{
// Set up gaze tracking.
TryEnableGazeTrackingAsync(args.Device);
}
/// <summary>
/// Handles disconnection of eye-tracking devices.
/// </summary>
/// <param name="sender">Source of the device removed event</param>
/// <param name="e">Event args for the device removed event</param>
private void DeviceRemoved(GazeDeviceWatcherPreview source,
GazeDeviceWatcherRemovedPreviewEventArgs args)
{
// Decrement gaze device counter and remove event handlers.
if (IsSupportedDevice(args.Device))
{
deviceCounter--;
TrackerCounter.Text = deviceCounter.ToString();
if (deviceCounter == 0)
{
gazeInputSource.GazeEntered -= this.GazeEntered;
gazeInputSource.GazeMoved -= this.GazeMoved;
gazeInputSource.GazeExited -= this.GazeExited;
}
}
}
Aquí, comprobamos si el dispositivo es viable en
IsSupportedDevice
y, si es así, intentamos habilitar el seguimiento de miradas enTryEnableGazeTrackingAsync
.En
TryEnableGazeTrackingAsync
, declaramos los controladores de eventos de mirada y llamamos a GazeInputSourcePreview.GetForCurrentView() para obtener una referencia al origen de entrada (se debe llamar a en el subproceso de la interfaz de usuario, consulte Mantener la capacidad de respuesta del subproceso de interfaz de usuario).Nota:
Debe llamar a GazeInputSourcePreview.GetForCurrentView() solo cuando un dispositivo compatible de seguimiento ocular esté conectado y requerido por la aplicación. De lo contrario, el cuadro de diálogo de consentimiento no es necesario.
/// <summary>
/// Initialize gaze tracking.
/// </summary>
/// <param name="gazeDevice"></param>
private async void TryEnableGazeTrackingAsync(GazeDevicePreview gazeDevice)
{
// If eye-tracking device is ready, declare event handlers and start tracking.
if (IsSupportedDevice(gazeDevice))
{
timerGaze.Interval = new TimeSpan(0, 0, 0, 0, 20);
timerGaze.Tick += TimerGaze_Tick;
SetGazeTargetLocation();
// This must be called from the UI thread.
gazeInputSource = GazeInputSourcePreview.GetForCurrentView();
gazeInputSource.GazeEntered += GazeEntered;
gazeInputSource.GazeMoved += GazeMoved;
gazeInputSource.GazeExited += GazeExited;
}
// Notify if device calibration required.
else if (gazeDevice.ConfigurationState ==
GazeDeviceConfigurationStatePreview.UserCalibrationNeeded ||
gazeDevice.ConfigurationState ==
GazeDeviceConfigurationStatePreview.ScreenSetupNeeded)
{
// Device isn't calibrated, so invoke the calibration handler.
System.Diagnostics.Debug.WriteLine(
"Your device needs to calibrate. Please wait for it to finish.");
await gazeDevice.RequestCalibrationAsync();
}
// Notify if device calibration underway.
else if (gazeDevice.ConfigurationState ==
GazeDeviceConfigurationStatePreview.Configuring)
{
// Device is currently undergoing calibration.
// A device update is sent when calibration complete.
System.Diagnostics.Debug.WriteLine(
"Your device is being configured. Please wait for it to finish");
}
// Device is not viable.
else if (gazeDevice.ConfigurationState == GazeDeviceConfigurationStatePreview.Unknown)
{
// Notify if device is in unknown state.
// Reconfigure/recalbirate the device.
System.Diagnostics.Debug.WriteLine(
"Your device is not ready. Please set up your device or reconfigure it.");
}
}
/// <summary>
/// Check if eye-tracking device is viable.
/// </summary>
/// <param name="gazeDevice">Reference to eye-tracking device.</param>
/// <returns>True, if device is viable; otherwise, false.</returns>
private bool IsSupportedDevice(GazeDevicePreview gazeDevice)
{
TrackerState.Text = gazeDevice.ConfigurationState.ToString();
return (gazeDevice.CanTrackEyes &&
gazeDevice.ConfigurationState ==
GazeDeviceConfigurationStatePreview.Ready);
}
A continuación, configuramos nuestros controladores de eventos de mirada.
Mostramos y ocultamos la elipse de seguimiento de mirada en
GazeEntered
yGazeExited
, respectivamente.En
GazeMoved
, movemos nuestra elipse de seguimiento de miradas basada en la EyeGazePosition proporcionada por el CurrentPoint del GazeEnteredPreviewEventArgs. También administramos el temporizador de foco de mirada en radialProgressBar, que desencadena la reposición de la barra de progreso. Consulte el paso siguiente para obtener más información./// <summary> /// GazeEntered handler. /// </summary> /// <param name="sender">Source of the gaze entered event</param> /// <param name="e">Event args for the gaze entered event</param> private void GazeEntered( GazeInputSourcePreview sender, GazeEnteredPreviewEventArgs args) { // Show ellipse representing gaze point. eyeGazePositionEllipse.Visibility = Visibility.Visible; // Mark the event handled. args.Handled = true; } /// <summary> /// GazeExited handler. /// Call DisplayRequest.RequestRelease to conclude the /// RequestActive called in GazeEntered. /// </summary> /// <param name="sender">Source of the gaze exited event</param> /// <param name="e">Event args for the gaze exited event</param> private void GazeExited( GazeInputSourcePreview sender, GazeExitedPreviewEventArgs args) { // Hide gaze tracking ellipse. eyeGazePositionEllipse.Visibility = Visibility.Collapsed; // Mark the event handled. args.Handled = true; } /// <summary> /// GazeMoved handler translates the ellipse on the canvas to reflect gaze point. /// </summary> /// <param name="sender">Source of the gaze moved event</param> /// <param name="e">Event args for the gaze moved event</param> private void GazeMoved(GazeInputSourcePreview sender, GazeMovedPreviewEventArgs args) { // Update the position of the ellipse corresponding to gaze point. if (args.CurrentPoint.EyeGazePosition != null) { double gazePointX = args.CurrentPoint.EyeGazePosition.Value.X; double gazePointY = args.CurrentPoint.EyeGazePosition.Value.Y; double ellipseLeft = gazePointX - (eyeGazePositionEllipse.Width / 2.0f); double ellipseTop = gazePointY - (eyeGazePositionEllipse.Height / 2.0f) - (int)Header.ActualHeight; // Translate transform for moving gaze ellipse. TranslateTransform translateEllipse = new TranslateTransform { X = ellipseLeft, Y = ellipseTop }; eyeGazePositionEllipse.RenderTransform = translateEllipse; // The gaze point screen location. Point gazePoint = new Point(gazePointX, gazePointY); // Basic hit test to determine if gaze point is on progress bar. bool hitRadialProgressBar = DoesElementContainPoint( gazePoint, GazeRadialProgressBar.Name, GazeRadialProgressBar); // Use progress bar thickness for visual feedback. if (hitRadialProgressBar) { GazeRadialProgressBar.Thickness = 10; } else { GazeRadialProgressBar.Thickness = 4; } // Mark the event handled. args.Handled = true; } }
Por último, estos son los métodos que se usan para administrar el temporizador de foco de mirada para esta aplicación.
DoesElementContainPoint
comprueba si el puntero de mirada está sobre la barra de progreso. Si es así, inicia el temporizador de mirada e incrementa la barra de progreso en cada tic del temporizador de mirada.SetGazeTargetLocation
establece la ubicación inicial de la barra de progreso y, si la barra de progreso se completa (dependiendo del temporizador del foco de mirada), mueve la barra de progreso a una ubicación aleatoria./// <summary> /// Return whether the gaze point is over the progress bar. /// </summary> /// <param name="gazePoint">The gaze point screen location</param> /// <param name="elementName">The progress bar name</param> /// <param name="uiElement">The progress bar UI element</param> /// <returns></returns> private bool DoesElementContainPoint( Point gazePoint, string elementName, UIElement uiElement) { // Use entire visual tree of progress bar. IEnumerable<UIElement> elementStack = VisualTreeHelper.FindElementsInHostCoordinates(gazePoint, uiElement, true); foreach (UIElement item in elementStack) { //Cast to FrameworkElement and get element name. if (item is FrameworkElement feItem) { if (feItem.Name.Equals(elementName)) { if (!timerStarted) { // Start gaze timer if gaze over element. timerGaze.Start(); timerStarted = true; } return true; } } } // Stop gaze timer and reset progress bar if gaze leaves element. timerGaze.Stop(); GazeRadialProgressBar.Value = 0; timerStarted = false; return false; } /// <summary> /// Tick handler for gaze focus timer. /// </summary> /// <param name="sender">Source of the gaze entered event</param> /// <param name="e">Event args for the gaze entered event</param> private void TimerGaze_Tick(object sender, object e) { // Increment progress bar. GazeRadialProgressBar.Value += 1; // If progress bar reaches maximum value, reset and relocate. if (GazeRadialProgressBar.Value == 100) { SetGazeTargetLocation(); } } /// <summary> /// Set/reset the screen location of the progress bar. /// </summary> private void SetGazeTargetLocation() { // Ensure the gaze timer restarts on new progress bar location. timerGaze.Stop(); timerStarted = false; // Get the bounding rectangle of the app window. Rect appBounds = Windows.UI.ViewManagement.ApplicationView.GetForCurrentView().VisibleBounds; // Translate transform for moving progress bar. TranslateTransform translateTarget = new TranslateTransform(); // Calculate random location within gaze canvas. Random random = new Random(); int randomX = random.Next( 0, (int)appBounds.Width - (int)GazeRadialProgressBar.Width); int randomY = random.Next( 0, (int)appBounds.Height - (int)GazeRadialProgressBar.Height - (int)Header.ActualHeight); translateTarget.X = randomX; translateTarget.Y = randomY; GazeRadialProgressBar.RenderTransform = translateTarget; // Show progress bar. GazeRadialProgressBar.Visibility = Visibility.Visible; GazeRadialProgressBar.Value = 0; }