Menatap interaksi dan pelacakan mata di aplikasi Windows

Pahlawan pelacakan mata

Memberikan dukungan untuk melacak tatapan, perhatian, dan kehadiran pengguna berdasarkan lokasi dan pergerakan mata mereka.

Catatan

Untuk melihat input dalam Windows Mixed Reality, lihat [Tatapan]/windows/mixed-reality/mrtk-unity/features/input/gaze).

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

Gambaran Umum

Input tatapan adalah cara yang ampuh untuk berinteraksi dan menggunakan aplikasi Windows yang sangat berguna sebagai teknologi bantuan untuk pengguna dengan penyakit neuro-otot (seperti ALS) dan disabilitas lain yang melibatkan gangguan fungsi otot atau saraf.

Selain itu, input tatapan menawarkan peluang yang sama menarik untuk game (termasuk akuisisi dan pelacakan target) dan aplikasi produktivitas tradisional, kios, dan skenario interaktif lainnya di mana perangkat input tradisional (keyboard, mouse, sentuhan) tidak tersedia, atau di mana mungkin berguna/membantu untuk membebaskan tangan pengguna untuk tugas lain (seperti memegang tas belanja).

Catatan

Dukungan untuk perangkat keras pelacakan mata diperkenalkan di Windows 10 Fall Creators Update bersama dengan kontrol Eye, fitur bawaan yang memungkinkan Anda menggunakan mata Anda untuk mengontrol penunjuk di layar, mengetik dengan keyboard di layar, dan berkomunikasi dengan orang-orang menggunakan teks ke ucapan. Sekumpulan WINDOWS Runtime API (Windows.Devices.Input.Preview) untuk membangun aplikasi yang dapat berinteraksi dengan perangkat keras pelacakan mata tersedia dengan Windows 10 Pembaruan April 2018 (Versi 1803, build 17134) dan yang lebih baru.

Privasi

Karena data pribadi yang berpotensi sensitif yang dikumpulkan oleh perangkat pelacakan mata, Anda diharuskan untuk mendeklarasikan gazeInput kemampuan dalam manifes aplikasi aplikasi Anda (lihat bagian Penyiapan berikut). Saat dinyatakan, Windows secara otomatis meminta pengguna dengan dialog persetujuan (saat aplikasi pertama kali dijalankan), di mana pengguna harus memberikan izin bagi aplikasi untuk berkomunikasi dengan perangkat pelacakan mata dan mengakses data ini.

Selain itu, jika aplikasi Anda mengumpulkan, menyimpan, atau mentransfer data pelacakan mata, Anda harus menjelaskannya dalam pernyataan privasi aplikasi Anda dan mengikuti semua persyaratan lain untuk Informasi Pribadi dalam Perjanjian Pengembang Aplikasi dan Kebijakan Microsoft Store.

Siapkan

Untuk menggunakan API input tatapan di aplikasi Windows, Anda harus:

  • Tentukan gazeInput kemampuan dalam manifes aplikasi.

    Buka file Package.appxmanifest dengan perancang manifes Visual Studio, atau tambahkan kemampuan secara manual dengan memilih Tampilkan kode, dan sisipkan yang berikut ini DeviceCapability ke dalam simpul Capabilities :

    <Capabilities>
       <DeviceCapability Name="gazeInput" />
    </Capabilities>
    
  • Perangkat pelacakan mata yang kompatibel dengan Windows yang terhubung ke sistem Anda (baik bawaan atau periferal) dan diaktifkan.

    Lihat Mulai menggunakan kontrol mata di Windows 10 untuk daftar perangkat pelacakan mata yang didukung.

Pelacakan mata dasar

Dalam contoh ini, kami menunjukkan cara melacak tatapan pengguna dalam aplikasi Windows dan menggunakan fungsi pengaturan waktu dengan pengujian hit dasar untuk menunjukkan seberapa baik mereka dapat mempertahankan fokus tatapan mereka pada elemen tertentu.

Elips kecil digunakan untuk menunjukkan di mana titik tatapan berada dalam viewport aplikasi, sementara RadialProgressBar dari Windows Community Toolkit ditempatkan secara acak di kanvas. Ketika fokus tatapan terdeteksi pada bilah kemajuan, timer dimulai dan bilah kemajuan secara acak direlokasi pada kanvas ketika bilah kemajuan mencapai 100%.

Pelacakan tatapan dengan sampel timer

Pelacakan tatapan dengan sampel timer

Unduh sampel ini dari sampel input Tatapan (dasar)

  1. Pertama, kami menyiapkan UI (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. Selanjutnya, kami menginisialisasi aplikasi kami.

    Dalam cuplikan ini, kami mendeklarasikan objek global kami dan mengambil alih peristiwa halaman OnNavigatedTo untuk memulai pengamat perangkat tatapan kami dan peristiwa halaman OnNavigatedFrom untuk menghentikan pengamat perangkat tatapan kami.

    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. Selanjutnya, kami menambahkan metode pengamat perangkat tatapan kami.

    Dalam StartGazeDeviceWatcher, kami memanggil CreateWatcher dan mendeklarasikan pendengar peristiwa perangkat pelacakan mata (DeviceAdded, DeviceUpdated, dan DeviceRemoved).

    Dalam DeviceAdded, kami memeriksa status perangkat pelacakan mata. Jika perangkat yang layak, kami meningkatkan jumlah perangkat kami dan mengaktifkan pelacakan tatapan. Lihat langkah berikutnya untuk detailnya.

    Dalam DeviceUpdated, kami juga mengaktifkan pelacakan tatapan saat peristiwa ini dipicu jika perangkat dikalibrasi ulang.

    Dalam DeviceRemoved, kami mengurangi penghitung perangkat kami dan menghapus penanganan aktivitas perangkat.

    Dalam StopGazeDeviceWatcher, kita mematikan pengamat perangkat tatapan.

    /// <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. Di sini, kami memeriksa apakah perangkat layak masuk IsSupportedDevice dan, jika demikian, mencoba mengaktifkan pelacakan tatapan di TryEnableGazeTrackingAsync.

    Dalam TryEnableGazeTrackingAsync, kami mendeklarasikan penanganan aktivitas tatapan, dan memanggil GazeInputSourcePreview.GetForCurrentView() untuk mendapatkan referensi ke sumber input (ini harus dipanggil pada utas UI, lihat Menjaga utas UI tetap responsif).

    Catatan

    Anda harus memanggil GazeInputSourcePreview.GetForCurrentView() hanya ketika perangkat pelacakan mata yang kompatibel terhubung dan diperlukan oleh aplikasi Anda. Jika tidak, dialog persetujuan tidak perlu.

    /// <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. Selanjutnya, kami menyiapkan penanganan aktivitas tatapan kami.

    Kami menampilkan dan menyembunyikan elips pelacakan tatapan di GazeEntered dan GazeExited, masing-masing.

    Dalam GazeMoved, kami memindahkan elips pelacakan tatapan kami berdasarkan EyeGazePosition yang disediakan oleh CurrentPoint dari GazeEnteredPreviewEventArgs. Kami juga mengelola timer fokus tatapan pada RadialProgressBar, yang memicu reposisi bilah kemajuan. Lihat langkah berikutnya untuk detailnya.

    /// <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. Terakhir, berikut adalah metode yang digunakan untuk mengelola timer fokus tatapan untuk aplikasi ini.

    DoesElementContainPoint memeriksa apakah penunjuk tatapan berada di atas bilah kemajuan. Jika demikian, itu memulai timer tatapan dan menaikkan bilah kemajuan pada setiap timer tatapan centang.

    SetGazeTargetLocation mengatur lokasi awal bilah kemajuan dan, jika bilah kemajuan selesai (tergantung pada timer fokus tatapan), memindahkan bilah kemajuan ke lokasi acak.

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

Lihat juga

Sumber

Sampel topik