Interazioni con lo sguardo fisso e tracciamento oculare nelle app Windows
Supportare il tracciamento dello sguardo fisso, dell'attenzione e della presenza di un utente in base alla posizione e al movimento dei suoi occhi.
Nota
Per l'input dello sguardo fisso in Windows Mixed Reality, vedere [Gaze]/windows/mixed-reality/mrtk-unity/features/input/gaze).
API importanti: Windows.Devices.Input.Preview, GazeDevicePreview, GazePointPreview, GazeInputSourcePreview
Panoramica
L'input dello sguardo fisso è un modo molto efficace per interagire e usare le applicazioni Windows, particolarmente utile come tecnologia assistiva per gli utenti con malattie neuro-muscolari (come la SLA) e altre disabilità che compromettono le funzioni muscolari o nervose.
Inoltre, l'input dello sguardo fisso offre opportunità altrettanto interessanti sia per i giochi (inclusi il tracciamento e l'acquisizione di destinazione) sia per le applicazioni di produttività tradizionali, i chioschi multimediali e altri scenari interattivi in cui i dispositivi di input tradizionali (tastiera, mouse, tocco) non sono disponibili o dove potrebbe essere utile avere le mani libere per altre attività (ad esempio tenere le borse della spesa).
Nota
Il supporto per gli eye tracker è stato introdotto in Windows 10 Fall Creators Update insieme al controllo Eye, una funzione predefinita che consente di usare gli occhi per controllare il puntatore sullo schermo, digitare con la tastiera su schermo e comunicare con le persone usando la sintesi vocale. Un set di API di Windows Runtime (Windows.Devices.Input.Preview) per creare applicazioni in grado di interagire con gli eye tracker è disponibile con l'aggiornamento di Windows 10 di aprile 2018 (Versione 1803, build 17134) e versioni successive.
Privacy
Considerando la possibilità che gli eye tracker raccolgano dati personali potenzialmente riservati, è necessario dichiarare la funzionalità gazeInput
nel manifesto dell'app dell'applicazione (vedere la sezione Configurazione). Dopo questa dichiarazione, Windows propone automaticamente agli utenti una finestra di dialogo di consenso (alla prima esecuzione dell'app), in cui l'utente deve concedere all'app l'autorizzazione a comunicare con il dispositivo di tracciamento oculare e accedere a questi dati.
Inoltre, se l'app raccoglie, archivia o trasferisce i dati raccolti tramite tracciamento oculare, è necessario descrivere tutto questo nell'informativa sulla privacy dell'app e seguire altri requisiti associati alle Informazioni personali nel Contratto per gli sviluppatori di app e nei Criteri di Microsoft Store.
Attrezzaggio
Per usare le API dell'input dello sguardo fisso nell'app Windows, sarà necessario:
Specificare la funzionalità
gazeInput
nel manifesto dell'app.Aprire il file Package.appxmanifest con la finestra di progettazione del manifesto di Visual Studio oppure aggiungere manualmente la funzionalità selezionando Visualizza codice e inserendo
DeviceCapability
nel nodoCapabilities
:<Capabilities> <DeviceCapability Name="gazeInput" /> </Capabilities>
Un dispositivo di tracciamento oculare compatibile con Windows connesso al sistema (predefinito o periferico) e attivato.
Per un elenco di dispositivi di tracciamento oculare supportati, vedere Attività iniziali con il controllo eye in Windows 10.
Tracciamento oculare di base
Questo esempio mostra come tracciare lo sguardo fisso dell'utente all'interno di un'app Windows e usare una funzione di timing con hit test di base per indicare quanto può mantenere lo sguardo fisso su un elemento specifico.
Si utilizza una piccola ellisse per mostrare dove si trova il punto dello sguardo fisso all'interno del riquadro di visualizzazione dell'applicazione, mentre l'oggetto RadialProgressBar incluso nel Windows Community Toolkit viene posizionato a caso nell'area di disegno. Quando si rileva lo stato attivo dello sguardo fisso sull'indicatore di stato, viene avviato un timer e l'indicatore di stato viene riposizionato in modo casuale nell'area di disegno quando raggiunge il 100%.
Esempio di tracciamento dello sguardo fisso con timer
Scaricare questo esempio dall'esempio di input dello sguardo fisso (di base)
Prima di tutto, si imposta l'interfaccia utente (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>
Successivamente, si inizializza l'app.
In questo frammento di codice si dichiarano gli oggetti globali e si esegue l'override dell'evento della pagina OnNavigatedTo per avviare il watcher del dispositivo basato su sguardo fisso e l'evento della pagina OnNavigatedFrom per arrestare il watcher del dispositivo basato su sguardo fisso.
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(); } } }
Successivamente, si aggiungono i metodi del watcher del dispositivo basato su sguardo fisso.
In
StartGazeDeviceWatcher
si chiama CreateWatcher e si dichiarano i listener eventi del dispositivo di tracciamento oculare (DeviceAdded, DeviceUpdated e DeviceRemoved).In
DeviceAdded
si controlla lo stato del dispositivo di tracciamento oculare. In caso di dispositivo adatto, si incrementa il conteggio dei dispositivi e si abilita il tracciamento dello sguardo fisso. Per dettagli, vedere il passaggio successivo.In
DeviceUpdated
è anche possibile abilitare il tracciamento dello sguardo fisso perché questo evento viene attivato quando un dispositivo viene ricalibrato.In
DeviceRemoved
si decrementa il contatore dei dispositivi e si rimuovono i gestori degli eventi del dispositivo.In
StopGazeDeviceWatcher
si arresta il watcher del dispositivo dello sguardo fisso.
/// <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;
}
}
}
Qui si verifica se il dispositivo è adatto in
IsSupportedDevice
e se sì tentare di abilitare il tracciamento dello sguardo fisso inTryEnableGazeTrackingAsync
.In
TryEnableGazeTrackingAsync
si dichiarano i gestori degli eventi sguardo fisso e si chiama GazeInputSourcePreview.GetForCurrentView() per ottenere un riferimento all'origine di input (deve essere chiamato sul thread dell'interfaccia utente, vedere Mantenere reattivo il thread dell'interfaccia utente).Nota
È consigliabile chiamare GazeInputSourcePreview.GetForCurrentView() solo quando un dispositivo di tracciamento oculare compatibile è connesso e richiesto dall'applicazione. In caso contrario, la finestra di dialogo di consenso non è necessaria.
/// <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);
}
Successivamente, si impostano i gestori degli eventi sguardo fisso.
Si visualizza e si nasconde l'ellisse di tracciamento dello sguardo fisso rispettivamente in
GazeEntered
eGazeExited
.In
GazeMoved
si sposta l'ellisse di tracciamento dello sguardo fisso su EyeGazePosition fornito da CurrentPoint di GazeEnteredPreviewEventArgs. Si gestisce anche il timer dello stato attivo dello sguardo fisso su RadialProgressBar, che attiva il riposizionamento dell'indicatore di stato. Per dettagli, vedere il passaggio successivo./// <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; } }
Infine, ecco i metodi usati per gestire il timer dello stato attivo dello sguardo fisso per questa app.
DoesElementContainPoint
controlla se il puntatore dello sguardo fisso si trova sull'indicatore di stato. In questo caso, avvia il timer dello sguardo fisso e incrementa l'indicatore di stato a ogni tick del timer dello sguardo fisso.SetGazeTargetLocation
imposta la posizione iniziale dell'indicatore di stato e, se quest'ultimo viene completato (a seconda del timer dello stato attivo dello sguardo fisso), viene spostato in una posizione casuale./// <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; }