Взаимодействие с взглядами и отслеживание глаз в приложениях Windows

Герой отслеживания глаз

Обеспечивает поддержку отслеживания взгляда, внимания и присутствия пользователя на основе расположения и перемещения глаз.

Примечание.

Сведения о входных данных в Windows Смешанная реальность см. в разделе [Gaze]/windows/mixed-reality/mrtk-unity/features/input/gaze).

Важные API: Windows.Devices.Input.Preview, GazeDevicePreview, GazePointPreview, GazeInputSourcePreview

Обзор

Входные данные взгляда — это мощный способ взаимодействия и использования приложений Windows, которые особенно полезны в качестве вспомогательной технологии для пользователей с нейро-мышечной болезнью (например, ALS) и другими ограниченными возможностями, связанными с нарушениями мышц или нервных функций.

Кроме того, входные данные взгляда предлагают одинаково убедительные возможности для игр (включая целевое приобретение и отслеживание) и традиционных приложений для повышения производительности, киосков и других интерактивных сценариев, где традиционные устройства ввода (клавиатура, мышь, сенсорный ввод) недоступны, или где это может быть полезно или полезно, чтобы освободить руки пользователя для других задач (например, держать сумки для покупок).

Примечание.

Поддержка оборудования отслеживания глаз появилась в Windows 10 Fall Creators Update вместе с элементом управления глаз, встроенной функцией, которая позволяет использовать глаза для управления указателем на экране, введите экранную клавиатуру и общайтесь с людьми, использующими текст в речь. Набор API среда выполнения Windows (Windows.Devices.Input.Preview) для создания приложений, которые могут взаимодействовать с оборудованием отслеживания глаз, доступны в обновлении Windows 10 апреля 2018 г. (версия 1803, сборка 17134) и более поздние версии.

Конфиденциальность

Из-за потенциально конфиденциальных персональных данных, собранных устройствами отслеживания глаз, необходимо объявить gazeInput возможность в манифесте приложения (см. следующий раздел установки ). При объявлении Windows автоматически запрашивает пользователям диалоговое окно согласия (при первом запуске приложения), где пользователь должен предоставить приложению разрешение на взаимодействие с устройством отслеживания глаз и получить доступ к этим данным.

Кроме того, если ваше приложение собирает, хранит или передает данные отслеживания глаз, необходимо описать это в заявлении о конфиденциальности приложения и следовать всем другим требованиям к персональным данным в соглашении разработчика приложений и политиках Microsoft Store.

Настройка

Чтобы использовать интерфейсы API ввода взгляда в приложении Windows, вам потребуется:

  • gazeInput Укажите возможность в манифесте приложения.

    Откройте файл Package.appxmanifest с помощью конструктора манифестов Visual Studio или добавьте его вручную, выбрав код представления и вставив следующий DeviceCapability код в Capabilities узел:

    <Capabilities>
       <DeviceCapability Name="gazeInput" />
    </Capabilities>
    
  • Устройство отслеживания глаз, совместимое с Windows, подключенное к системе (встроенное или периферийное устройство) и включено.

    Ознакомьтесь со статьей "Начало работы с контролем глаз" в Windows 10 для списка поддерживаемых устройств отслеживания глаз.

Базовое отслеживание глаз

В этом примере мы покажем, как отслеживать взгляд пользователя в приложении Windows и использовать функцию времени с базовым тестированием попаданий, чтобы указать, насколько хорошо они могут поддерживать фокус взгляда на определенный элемент.

Небольшая многоточие используется для отображения того, где точка взгляда находится в области просмотра приложения, в то время как RadialProgressBar из набора средств сообщества Windows размещается случайным образом на холсте. При обнаружении фокуса взгляда на панели хода выполнения запускается таймер и индикатор хода выполнения случайно перемещается на холсте, когда индикатор хода выполнения достигает 100 %.

Отслеживание взгляда с примером таймера

Отслеживание взгляда с примером таймера

Скачайте этот пример из примера входных данных Gaze (базовый)

  1. Сначала мы настраиваем пользовательский интерфейс (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. Затем мы инициализируем наше приложение.

    В этом фрагменте мы объявляем наши глобальные объекты и переопределяем событие страницы OnNavigatedTo, чтобы запустить наблюдатель за взглядом и событие страницы OnNavigatedFrom, чтобы остановить наблюдателя за взглядом устройства.

    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. Затем мы добавим методы наблюдателя за устройствами взгляда.

    В StartGazeDeviceWatcherэтом случае мы вызываем CreateWatcher и объявляем прослушиватели событий устройства отслеживания глаз (DeviceAdded, DeviceUpdated и DeviceRemoved).

    В DeviceAddedэтом случае мы проверяем состояние устройства отслеживания глаз. Если жизнеспособное устройство, мы добавим число устройств и включите отслеживание взгляда. Дополнительные сведения см. на следующем шаге.

    В DeviceUpdatedэтом случае мы также включите отслеживание взгляда, так как это событие активируется, если устройство переназначается.

    В DeviceRemovedэтом случае мы уменьшаем счетчик устройств и удаляем обработчики событий устройства.

    В StopGazeDeviceWatcherэтом случае мы закрываем наблюдатель за взглядом.

    /// <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. Здесь мы проверяем, является ли устройство жизнеспособным IsSupportedDevice и, если да, попытаемся включить отслеживание взгляда в TryEnableGazeTrackingAsync.

    В TryEnableGazeTrackingAsync, мы объявляем обработчики событий взгляда и вызываем GazeInputSourcePreview.GetForCurrentView(), чтобы получить ссылку на источник входных данных (это должно быть вызвано в потоке пользовательского интерфейса, см. статью "Сохранить отклик потока пользовательского интерфейса").

    Примечание.

    Необходимо вызывать GazeInputSourcePreview.GetForCurrentView() только в том случае, если совместимое устройство отслеживания глаз подключено и требуется для приложения. В противном случае диалоговое окно согласия не требуется.

    /// <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. Затем мы настроили обработчики событий взгляда.

    Мы отображаем и скрываем многоточие отслеживания взгляда и GazeEntered GazeExitedсоответственно.

    ВGazeMoved, мы перемещаем многоточие отслеживания взгляда на основе EyeGazePosition, предоставленной CurrentPoint объекта GazeEnteredPreviewEventArgs. Мы также управляем таймером фокуса взгляда на RadialProgressBar, который активирует изменение положения индикатора выполнения. Дополнительные сведения см. на следующем шаге.

    /// <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. Наконец, вот методы, используемые для управления таймером фокусировки взгляда для этого приложения.

    DoesElementContainPoint проверяет, находится ли указатель взгляда на индикатор хода выполнения. Если да, он запускает таймер взгляда и увеличивает индикатор хода выполнения на каждом таймере взгляда.

    SetGazeTargetLocation задает начальное расположение панели выполнения и, если индикатор выполнения завершается (в зависимости от таймера фокусировки взгляда), перемещает индикатор выполнения в случайное расположение.

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

См. также

Ресурсы

Примеры в статье