Windows 應用程式中的注視互動和眼動追蹤
為根據使用者眼睛的位置及移動,追蹤使用者的注視、注意力和狀態提供支援。
注意
如需 Windows Mixed Reality 中的注視輸入,請參閱 [Gaze]/windows/mixed-reality/mrtk-unity/features/input/gaze)。
重要 API:Windows.Devices.Input.Preview、GazeDevicePreview、GazePointPreview、GazeInputSourcePreview
概觀
注視輸入是互動和使用 Windows 應用程式的強大方式,對患有神經肌肉疾病 (如 ALS) 和其他有肌肉或神經功能受損的殘疾使用者來說,特別有用。
此外,注視輸入還為遊戲 (包括目標獲取和追蹤) 和傳統生產力應用程式、資訊亭及其他互動式場景提供同樣具有吸引力的機會,其他互動式場景包括無法使用傳統輸入裝置 (鍵盤、滑鼠、觸控) 或讓使用者空出雙手執行其他任務 (例如:提購物袋) 可能很有用/有幫助的情況。
注意
Windows 10 Fall Creators Update 中引入對眼動追蹤硬體的支援以及眼球控制,這是一項內建功能,可讓您使用眼睛控制螢幕上的指標、使用螢幕小鍵盤輸入,並使用文字轉換語音與人們交流。 Windows 10 April 2018 Update (版本1803,組建17134) 及更新版本中提供一組 Windows 執行階段 API (Windows.Devices.Input.Preview),用於建立可與眼動追蹤硬體互動的應用程式。
隱私權
由於眼動追蹤裝置可能會收集敏感的個人資料,因此您需要在應用程式的應用程式清單中宣告 gazeInput
功能 (請參閱以下設定一節)。 宣告時,Windows 會自動向使用者提示同意對話方塊 (第一次執行應用程式時),使用者必須在對話方塊內為應用程式授與與眼動追蹤裝置互動以及存取此資料的權限。
此外,如果您的應用程式收集、儲存或傳輸眼動追蹤資料,您還必須在應用程式的隱私權聲明中加以說明,並遵循應用程式開發人員合約和 Microsoft Store 原則中的所有其他個人資訊相關要求。
設定
若要在 Windows 應用程式中使用注視輸入 API,您需要:
在應用程式資訊清單中指定
gazeInput
功能。使用 Visual Studio 資訊清單設計工具開啟 Package.appxmanifest 檔案,或透過選擇 [檢視程式碼] 並將以下
DeviceCapability
插入Capabilities
節點來手動新增該功能:<Capabilities> <DeviceCapability Name="gazeInput" /> </Capabilities>
與 Windows 相容的眼動追蹤裝置連接到你的系統 (內建或週邊) 並開啟。
如需支援的眼球追蹤裝置清單,請參閱Windows 10 中眼動控制入門。
基本眼動追蹤
在本範例中,我們示範如何在 Windows 應用程式中追蹤使用者的注視,並使用計時函式和基本點擊測試來指示他們將注視焦點保持在特定元素上的程度。
小橢圓用於顯示注視點在應用程式視窗內的位置,而 Windows Community Toolkit 中的 RadialProgressBar 則隨機放置在畫布上。 當在進度列上偵測到注視焦點時,就會啟動計時器,當進度列達到 100% 時,進度列會在畫布上隨機重新定位。
使用計時器進行注視追蹤範例
從注視輸入範例 (基本) 下載此範例
首先,我們設定 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>
接下來,我們將初始化應用程式。
在此程式碼片段中,我們宣告全域物件並覆寫 OnNavieratedTo 頁面事件以啟動注視裝置監看員,和並覆寫 OnNavieratedFrom 頁面事件以停止我們的注視裝置監看員。
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(); } } }
接下來,我們加入注視裝置監看員方法。
在
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;
}
}
}
在這裡,我們檢查裝置在
IsSupportedDevice
是否可用,如果可用,請嘗試在TryEnableGazeTrackingAsync
中啟用注視追蹤。在
TryEnableGazeTrackingAsync
中,我們宣告注視事件處理常式,並呼叫 GazeInputSourcePreview.GetForCurrentView () 以取得輸入來源的引用 (這必須在 UI 執行緒上呼叫,請參閱讓 UI 執行緒保持回應)。注意
只有在應用程式已連接相容的眼動追蹤裝置且需要時,才應該呼叫 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);
}
接下來,我們設定注視事件處理常式。
我們分別在
GazeEntered
和GazeExited
中顯示和隱藏注視追蹤橢圓。在
GazeMoved
中,我們根據 GazeEnteredPreviewEventArgs 的 CurrentPoint 提供的 EyeGazePosition 移動注視追蹤橢圓。 我們也管理 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; } }
最後,以下是用來管理此應用程式的注視焦點計時器的方法。
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; }