Blickinteraktionen und Eyetracking in Windows-Apps

Eyetracking Hero

Unterstützung für die Verfolgung von Blick, Aufmerksamkeit und Anwesenheit eines Benutzers basierend auf der Position und Bewegung seiner Augen.

Hinweis

Informationen zu Blickeingaben in Windows Mixed Reality finden Sie unter [Gaze]/windows/mixed-reality/mrtk-unity/features/input/gaze).

Wichtige APIs: Windows.Devices.Input.Preview, GazeDevicePreview, GazePointPreview, GazeInputSourcePreview

Übersicht

Blickeingabe ist eine leistungsstarke Möglichkeit zur Interaktion und Verwendung von Windows-Anwendungen, die als Hilfstechnologie für Benutzer mit Neuro-Muskelerkrankungen (z. B. ALS) und anderen Behinderungen mit beeinträchtigten Muskel- oder Nervenfunktionen nützlich ist.

Darüber hinaus bietet die Blickeingabe gleichermaßen überzeugende Möglichkeiten sowohl für Spiele (einschließlich Zielerfassung und -verfolgung) als auch für herkömmliche Produktivitätsanwendungen, Kioske und andere interaktive Szenarien, in denen herkömmliche Eingabegeräte (Tastatur, Maus, Toucheingabe) nicht verfügbar sind oder wo es nützlich/hilfreich sein kann, die Hände des Benutzers für andere Aufgaben (z. B. das Halten von Einkaufstaschen) freizugeben.

Hinweis

Unterstützung für Eyetracking-Hardware wurde in Windows 10 Fall Creators Update zusammen mit der Eye-Steuerung eingeführt, einem integrierten Feature, mit dem Sie den Zeiger auf dem Bildschirm, die Eingabe mit der Bildschirmtastatur und die Kommunikation mit Personen mithilfe von Text-zu-Sprache verwenden können. Eine Reihe von Windows-Runtime-APIs (Windows.Devices.Input.Preview) zum Erstellen von Anwendungen, die mit Eyetracking-Hardware interagieren können, ist ab Windows 10 Update vom April 2018 (Version 1803, Build 17134) verfügbar.

Datenschutz

Aufgrund der potenziell sensiblen personenbezogenen Daten, die von Eyetracking-Geräten gesammelt werden, müssen Sie die gazeInput Funktion im App-Manifest Ihrer Anwendung deklarieren (siehe den folgenden Abschnitt setup ). Wenn die App deklariert wird, fordert Windows Benutzer automatisch mit einem Zustimmungsdialogfeld auf (wenn die App zum ersten Mal ausgeführt wird), in dem der Benutzer der App die Berechtigung erteilen muss, mit dem Eyetracking-Gerät zu kommunizieren und auf diese Daten zuzugreifen.

Wenn Ihre App Eyetracking-Daten sammelt, speichert oder überträgt, müssen Sie dies in der Datenschutzerklärung Ihrer App beschreiben und alle anderen Anforderungen für personenbezogene Informationen in der Vereinbarung für App-Entwickler und den Microsoft Store-Richtlinien befolgen.

Einrichten

Um die Blickeingabe-APIs in Ihrer Windows-App verwenden zu können, benötigen Sie Folgendes:

  • Geben Sie die gazeInput Funktion im App-Manifest an.

    Öffnen Sie die Datei Package.appxmanifest mit dem Visual Studio-Manifest-Designer, oder fügen Sie die Funktion manuell hinzu, indem Sie Code anzeigen auswählen und Folgendes DeviceCapability in den Capabilities Knoten einfügen:

    <Capabilities>
       <DeviceCapability Name="gazeInput" />
    </Capabilities>
    
  • Ein Windows-kompatibles Eyetracking-Gerät, das mit Ihrem System (entweder integriert oder Peripheriegerät) verbunden und aktiviert ist.

    Eine Liste der unterstützten Eyetracking-Geräte finden Sie unter Erste Schritte mit der Augensteuerung in Windows 10.

Grundlegendes Eyetracking

In diesem Beispiel wird veranschaulicht, wie sie den Blick des Benutzers in einer Windows-App nachverfolgen und eine Zeitsteuerungsfunktion mit grundlegenden Treffertests verwenden, um anzugeben, wie gut der Blick auf ein bestimmtes Element aufrechterhalten kann.

Eine kleine Ellipse wird verwendet, um anzuzeigen, wo sich der Blickpunkt innerhalb des Anwendungs-Viewports befindet, während ein RadialProgressBar aus dem Windows Community Toolkit zufällig auf der Canvas platziert wird. Wenn der Blickfokus auf der Statusanzeige erkannt wird, wird ein Timer gestartet, und die Statusanzeige wird zufällig auf der Canvas verschoben, wenn die Statusanzeige 100 % erreicht.

Blickverfolgung mit Timerbeispiel

Blickverfolgung mit Timerbeispiel

Laden Sie dieses Beispiel aus dem Eingabebeispiel "Gaze" (Basic) herunter.

  1. Zunächst richten wir die Benutzeroberfläche (MainPage.xaml) ein.

    <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. Als Nächstes initialisieren wir unsere App.

    In diesem Codeausschnitt deklarieren wir unsere globalen Objekte und setzen das OnNavigatedTo-Seitenereignis außer Kraft, um unseren Blickgeräteüberwachungsmodus zu starten, und das OnNavigatedFrom-Seitenereignis , um unseren Blickgeräte-Watcher zu beenden.

    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. Als Nächstes fügen wir unsere Watcher-Methoden für das Blickgerät hinzu.

    In StartGazeDeviceWatcherrufen wir CreateWatcher auf und deklarieren die Eyetracking-Geräteereignislistener (DeviceAdded, DeviceUpdated und DeviceRemoved).

    In DeviceAddedüberprüfen wir den Zustand des Eyetrackinggeräts. Wenn es sich um ein funktionsfähiges Gerät handelt, erhöhen wir die Geräteanzahl und aktivieren die Nachverfolgung des Anvisierens. Weitere Informationen finden Sie im nächsten Schritt.

    In DeviceUpdatedaktivieren wir auch die Blickverfolgung, da dieses Ereignis ausgelöst wird, wenn ein Gerät neu kalibriert wird.

    In DeviceRemovedwird der Gerätezähler dekrementiert und die Geräteereignishandler entfernt.

    In StopGazeDeviceWatcherfahren wir den Watcher des Blickgeräts herunter.

    /// <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. Hier überprüfen wir, ob das Gerät in IsSupportedDevice funktionsfähig ist, und versuchen, die Nachverfolgung des Anvisierens in TryEnableGazeTrackingAsynczu aktivieren.

    In TryEnableGazeTrackingAsyncdeklarieren wir die Gaze-Ereignishandler und rufen GazeInputSourcePreview.GetForCurrentView() auf, um einen Verweis auf die Eingabequelle zu erhalten (dies muss im UI-Thread aufgerufen werden, siehe Beibehalten der Reaktionsfähigkeit des UI-Threads).

    Hinweis

    Sie sollten GazeInputSourcePreview.GetForCurrentView() nur aufrufen, wenn ein kompatibles Eyetracking-Gerät verbunden ist und von Ihrer Anwendung benötigt wird. Andernfalls ist das Zustimmungsdialogfeld nicht erforderlich.

    /// <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. Als Nächstes richten wir unsere Gaze-Ereignishandler ein.

    Die Blickverfolgungsellipse wird in GazeEntered bzw. angezeigt und GazeExitedausgeblendet.

    In GazeMovedverschieben wir unsere Blickverfolgungsellipse basierend auf der EyeGazePosition , die vom CurrentPoint der GazeEnteredPreviewEventArgs bereitgestellt wird. Wir verwalten auch den Blickfokustimer auf der RadialProgressBar, wodurch eine Neupositionierung der Statusanzeige ausgelöst wird. Weitere Informationen finden Sie im nächsten Schritt.

    /// <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. Schließlich finden Sie hier die Methoden, die zum Verwalten des Blickfokustimers für diese App verwendet werden.

    DoesElementContainPoint überprüft, ob sich der Blickzeiger über der Statusanzeige befindet. Wenn ja, wird der Blicktimer gestartet und die Statusanzeige auf jedem Blickzeitpunkt erhöht.

    SetGazeTargetLocation legt die anfängliche Position der Statusanzeige fest und verschiebt die Statusanzeige, wenn die Statusanzeige (abhängig vom Fokuszeitgeber für den Blick) abgeschlossen ist, an eine zufällige Position.

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

Siehe auch

Ressourcen

Themenbeispiele