Condividi tramite


Interazioni con sguardo fisso e tracciamento oculare nelle app di Windows

Eroe di tracciamento oculare

Fornire supporto per tenere traccia dello sguardo, dell'attenzione e della presenza di un utente in base alla posizione e allo spostamento degli occhi.

Annotazioni

Per l'input dello sguardo in Windows Mixed Reality, vedi [Sguardo]/windows/mixed-reality/mrtk-unity/features/input/gaze.

API importanti: Windows.Devices.Input.Preview, GazeDevicePreview, GazePointPreview, GazeInputSourcePreview

Informazioni generali

L'input oculare è un modo potente per interagire e usare applicazioni Windows che è particolarmente utile come tecnologia assistiva per gli utenti affetti da malattie neuro-muscolari (come l'ALS) e altre disabilità che coinvolgono funzioni muscolari o nervose compromesse.

Inoltre, l'input dello sguardo offre opportunità altrettanto interessanti sia per i giochi (inclusi l'acquisizione e il rilevamento di bersagli) che per le applicazioni di produttività tradizionali, chioschi e altri scenari interattivi in cui i dispositivi di input tradizionali (tastiera, mouse, tocco) non sono disponibili, o dove potrebbe essere utile/vantaggioso liberare le mani dell'utente per altre attività (ad esempio tenere in mano i sacchetti della spesa).

Annotazioni

Il supporto per l'hardware di tracciamento oculare è stato introdotto in Windows 10 Fall Creators Update insieme al controllo Eye, una funzionalità predefinita che consente di usare gli occhi per controllare il puntatore sullo schermo, digitare con la tastiera su schermo e comunicare con le persone che usano la sintesi vocale. Un set di API Windows Runtime (Windows.Devices.Input.Preview) per la compilazione di applicazioni che possono interagire con l'hardware di tracciamento oculare è disponibile con Windows 10 April 2018 Update (versione 1803, build 17134) e versioni successive.

Riservatezza

A causa dei dati personali potenzialmente sensibili raccolti dai dispositivi di tracciamento oculare, è necessario dichiarare la gazeInput funzionalità nel manifesto dell'app dell'applicazione (vedere la sezione configurazione seguente). Quando dichiarato, Windows richiede automaticamente agli utenti una finestra di dialogo di consenso (quando l'app viene eseguita per la prima volta), in cui l'utente deve concedere all'app l'autorizzazione per comunicare con il dispositivo di tracciamento oculare e accedere a questi dati.

Inoltre, se l'app raccoglie, archivia o trasferisce i dati di tracciamento oculare, devi descriverli nell'informativa sulla privacy della tua app e seguire tutti gli altri requisiti per le informazioni personali nel Contratto per gli sviluppatori di app e nei criteri di Microsoft Store.

Configurazione

Per usare le API di input oculare nella tua app di Windows, dovrai:

  • Specificare la gazeInput funzionalità 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 quanto segue DeviceCapability nel Capabilities nodo:

    <Capabilities>
       <DeviceCapability Name="gazeInput" />
    </Capabilities>
    
  • Un dispositivo di tracciamento oculare compatibile con Windows connesso al sistema (integrato o periferico) e attivato.

    Per un elenco dei dispositivi di tracciamento oculare supportati, vedi Introduzione al controllo oculare in Windows 10 .

Tracciamento oculare di base

In questo esempio viene illustrato come tenere traccia dello sguardo fisso dell'utente all'interno di un'app di Windows e usare una funzione di temporizzazione con hit testing di base per indicare come mantenere lo sguardo fisso su un elemento specifico.

Un piccolo ellisse viene usato per mostrare dove si trova il punto di sguardo all'interno del riquadro di visualizzazione dell'applicazione, mentre un RadialProgressBar di Windows Community Toolkit viene posizionato in modo casuale sul canvas. Quando lo stato attivo dello sguardo fisso viene rilevato sulla barra di avanzamento, viene avviato un timer e la barra di avanzamento viene spostata in modo casuale sulla tela quando l'indicatore di avanzamento raggiunge il 100%.

Tracciamento dello sguardo con esempio di timer

Rilevamento dello sguardo fisso con esempio di timer

Scaricare questo esempio dal Gaze input sample (di base)

  1. Prima di tutto, impostiamo 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>
    
  2. A questo punto, inizializziamo l'app.

    In questo frammento di codice dichiariamo gli oggetti globali ed eseguiamo l'override dell'evento della pagina OnNavigatedTo per avviare il watcher del dispositivo di tracciamento dello sguardo e l'evento della pagina OnNavigatedFrom per arrestare il watcher del dispositivo di tracciamento dello sguardo.

    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();
            }
        }
    }
    
  3. Successivamente, aggiungiamo i metodi del dispositivo di rilevamento dello sguardo.

    In StartGazeDeviceWatcherchiamiamo CreateWatcher e dichiariamo i listener di eventi del dispositivo di tracciamento oculare (DeviceAdded, DeviceUpdated e DeviceRemoved).

    In DeviceAddedviene controllato lo stato del dispositivo di tracciamento oculare. Se il dispositivo è funzionante, incrementiamo il conteggio dei dispositivi e abilitiamo il tracciamento dello sguardo. Per informazioni dettagliate, vedere il passaggio successivo.

    In DeviceUpdated abilitiamo anche il rilevamento dello sguardo poiché questo evento viene attivato quando un dispositivo viene ricalibrato.

    In DeviceRemoveddecrementiamo il contatore dei dispositivi e rimuoviamo i gestori eventi del dispositivo.

    In StopGazeDeviceWatcher, il watcher del dispositivo di tracciamento dello sguardo viene arrestato.

    /// <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;
            }
        }
    }
  1. In questo caso, controlliamo se il dispositivo è operativo IsSupportedDevice e, in tal caso, proviamo ad abilitare il rilevamento dello sguardo in TryEnableGazeTrackingAsync.

    In TryEnableGazeTrackingAsyncdichiariamo i gestori di eventi di sguardo e chiamiamo GazeInputSourcePreview.GetForCurrentView() per ottenere un riferimento all'origine di input (questo deve essere chiamato nel thread dell'interfaccia utente, vedi Mantenere il thread dell'interfaccia utente reattivo).

    Annotazioni

    È 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);
    }
  1. Successivamente, impostiamo i gestori eventi sguardo.

    L'ellisse del tracciamento dello sguardo viene visualizzata e nascosta rispettivamente in GazeEntered e GazeExited.

    In GazeMoved spostiamo l'ellisse di tracciamento dello sguardo in base a EyeGazePosition fornito da CurrentPoint dell'oggetto GazeEnteredPreviewEventArgs. È anche possibile gestire il timer del fuoco dello sguardo su RadialProgressBar, che attiva il riposizionamento della barra di avanzamento. Per informazioni dettagliate, 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;
        }
    }
    
  2. Ecco infine i metodi usati per gestire il timer della messa a fuoco dello sguardo per questa app.

    DoesElementContainPoint controlla se il puntatore dello sguardo si trova sulla barra di progresso. In tal caso, avvia il timer dello sguardo e incrementa la barra di avanzamento a ogni tick del timer dello sguardo.

    SetGazeTargetLocation imposta la posizione iniziale della barra di avanzamento e, se la barra di avanzamento viene completata (a seconda del timer di messa a fuoco dello sguardo), sposta la barra di avanzamento 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;
    }
    

Vedere anche

Risorse

Esempi di argomenti