Udostępnij przez


Interakcje z wzrokiem i śledzenie wzroku w aplikacjach systemu Windows

Bohater śledzenia oczu

Zapewnienie obsługi śledzenia wzroku, uwagi i obecności użytkownika w oparciu o lokalizację i ruch oczu.

Uwaga / Notatka

Aby zapoznać się z danymi wejściowymi w środowisku Windows Mixed Reality, zobacz [Gaze]/windows/mixed-reality/mrtk-unity/features/input/gaze.

Ważne interfejsy API: Windows.Devices.Input.Preview, GazeDevicePreview, GazePointPreview, GazeInputSourcePreview

Przegląd

Sterowanie wzrokiem to zaawansowany sposób interakcji i korzystania z aplikacji systemu Windows, szczególnie przydatny jako technologia wspomagająca dla użytkowników z chorobami neuromięśniowymi (takimi jak ALS) i innymi niepełnosprawnościami obejmującymi zaburzenia mięśni lub czynności nerwowych.

Ponadto, wejście wzrokowe oferuje równie atrakcyjne możliwości zarówno dla gier (w tym namierzania celów i śledzenia), jak i tradycyjnych aplikacji produktywności, kiosków oraz innych interaktywnych scenariuszy, w których tradycyjne urządzenia wejściowe (klawiatura, mysz, dotyk) są niedostępne, lub gdzie może być przydatne/pomocne uwolnić ręce użytkownika do innych zadań (takich jak trzymanie torby na zakupy).

Uwaga / Notatka

Obsługa sprzętu do śledzenia oczu została wprowadzona w systemie Windows 10 Fall Creators Update wraz z kontrolką Eye, wbudowaną funkcją, która pozwala używać oczu do kontrolowania wskaźnika na ekranie, wpisywania za pomocą klawiatury ekranowej i komunikowania się z osobami korzystającymi z zamiany tekstu na mowę. Zestaw interfejsów API środowiska uruchomieniowego systemu Windows (Windows.Devices.Input.Preview) do tworzenia aplikacji, które mogą współdziałać ze sprzętem do śledzenia oczu, jest dostępny z aktualizacją systemu Windows 10 z kwietnia 2018 r. (wersja 1803, kompilacja 17134) i nowsze.

Prywatność

Ze względu na potencjalnie poufne dane osobowe zebrane przez urządzenia do śledzenia oczu, wymagane jest zadeklarowanie gazeInput możliwości w manifeście aplikacji (zobacz poniższą sekcję Konfiguracji). Po zadeklarowaniu system Windows automatycznie wyświetla użytkownikom okno dialogowe (gdy aplikacja jest uruchamiana po raz pierwszy), w którym użytkownik musi nadać aplikacji uprawnienia do komunikacji z urządzeniem śledzącym ruchy oczu i dostępu do tych danych.

Ponadto, jeśli aplikacja zbiera, przechowuje lub przesyła dane śledzenia oczu, musisz opisać je w zasadach zachowania poufności informacji w aplikacji i przestrzegać wszystkich innych wymagań dotyczących informacji osobistych w umowie dewelopera aplikacji i zasadach sklepu Microsoft Store.

Konfiguracja

Aby korzystać z interfejsów API spojrzenia jako wejścia w aplikacji systemu Windows, musisz podjąć następujące działania:

  • gazeInput Określ możliwość w manifeście aplikacji.

    Otwórz plik Package.appxmanifest za pomocą projektanta manifestu programu Visual Studio lub dodaj tę funkcję ręcznie, wybierając pozycję Wyświetl kod i wstaw następujące DeviceCapability elementy do węzła Capabilities :

    <Capabilities>
       <DeviceCapability Name="gazeInput" />
    </Capabilities>
    
  • Urządzenie do śledzenia oczu zgodne z systemem Windows podłączone do systemu (wbudowane lub peryferyjne) i włączone.

    Aby uzyskać listę obsługiwanych urządzeń do śledzenia oczu, zobacz Wprowadzenie do kontroli oczu w systemie Windows 10 .

Podstawowe śledzenie oczu

W tym przykładzie pokazujemy, jak śledzić wzrok użytkownika w aplikacji Windows i używać funkcji czasowej z podstawowym testowaniem trafności, aby wskazać, jak dobrze potrafią utrzymać uwagę na określonym elemencie.

Mały wielokropek służy do pokazywania miejsca, w którym punkt widzenia znajduje się w obszarze widoku aplikacji, podczas gdy pasek RadialProgressBar z zestawu narzędzi Windows Community Toolkit jest umieszczany losowo na kanwie. Po wykryciu fokusu na pasku postępu jest uruchamiany czasomierz, a pasek postępu jest losowo przenoszony na kanwie, gdy pasek postępu osiągnie 100%.

Śledzenie wzroku z próbką czasomierza

Śledzenie spojrzenia przy użyciu przykładu czasomierza

Pobierz ten przykład z przykładu danych wejściowych w usłudze Gaze (podstawowy)

  1. Najpierw skonfigurujemy interfejs użytkownika (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. Następnie zainicjujemy naszą aplikację.

    W tym fragmencie kodu zadeklarujemy nasze obiekty globalne i zastąpimy zdarzenie OnNavigatedTo strony, aby uruchomić obserwatora urządzenia z spojrzeniem, i zdarzenie OnNavigatedFrom strony, aby zatrzymać naszego obserwatora urządzenia z spojrzeniem.

    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. Następnie dodamy nasze metody obserwatora urządzenia do śledzenia wzroku.

    W StartGazeDeviceWatcher wywołujemy CreateWatcher i deklarujemy nasłuchiwacze zdarzeń dla urządzenia śledzącego wzrok (DeviceAdded, DeviceUpdated i DeviceRemoved).

    W DeviceAdded, sprawdzamy status urządzenia do śledzenia oczu. Jeśli jest to działające urządzenie, zwiększamy liczbę urządzeń i włączamy śledzenie spojrzenia. Aby uzyskać szczegółowe informacje, zobacz następny krok.

    W DeviceUpdated uruchamiamy także śledzenie spojrzeń, ponieważ to zdarzenie jest wyzwalane, jeśli urządzenie jest ponownie kalibrowane.

    W DeviceRemoved zmniejszamy licznik urządzeń i usuwamy programy obsługi zdarzeń urządzenia.

    W StopGazeDeviceWatcher zamknęliśmy obserwatora urządzeń wzrokowych.

    /// <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. W tym miejscu sprawdzamy, czy urządzenie jest funkcjonalne w IsSupportedDevice, a jeśli tak, spróbujemy włączyć śledzenie wzroku w TryEnableGazeTrackingAsync.

    W TryEnableGazeTrackingAsync deklarujemy programy obsługi zdarzeń spojrzenia i wywołujemy funkcję GazeInputSourcePreview.GetForCurrentView(), aby uzyskać odwołanie do źródła danych wejściowych (należy wywołać w wątku interfejsu użytkownika, zobacz Zachowaj responsywność wątku interfejsu użytkownika).

    Uwaga / Notatka

    Wywołaj metodę GazeInputSourcePreview.GetForCurrentView() tylko wtedy, gdy zgodne urządzenie do śledzenia oczu jest połączone i wymagane przez aplikację. W przeciwnym razie okno dialogowe zgody jest niepotrzebne.

    /// <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. Następnie skonfigurujemy obsługę zdarzeń spojrzenia.

    Wyświetlamy i ukrywamy elipsę śledzenia spojrzenia w GazeEntered i GazeExited, odpowiednio.

    W GazeMoved systemie przenosimy elipsę śledzenia wzroku na podstawie pozycji EyeGazePosition dostarczonej przez CurrentPointGazeEnteredPreviewEventArgs. Zarządzamy również czasomierzem fokusu wzroku na pasku RadialProgressBar, który wyzwala zmianę położenia paska postępu. Aby uzyskać szczegółowe informacje, zobacz następny krok.

    /// <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. Na koniec poniżej przedstawiono metody służące do zarządzania czasomierzem fokusu wzroku dla tej aplikacji.

    DoesElementContainPoint sprawdza, czy wskaźnik spojrzenia znajduje się na pasku postępu. Jeśli tak, uruchamia czasomierz wzroku i zwiększa pasek postępu na każdym znaczniku czasomierza wzroku.

    SetGazeTargetLocation Ustawia początkową lokalizację paska postępu, a jeśli pasek postępu zakończy się (w zależności od czasomierza fokusu spojrzenia), przenosi pasek postępu do losowej lokalizacji.

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

Zobacz także

Zasoby

Przykłady tematów