概述 Gaze Input & Tracking - 也就是視覺輸入和跟蹤,是一種和滑鼠/觸摸屏輸入非常不一樣的交互方式,利用人類眼球的識別和眼球方向角度的跟蹤,來判斷人眼的目標和意圖,從而非常方便的完成對設備的控制和操作。這種交互方式,應用場景非常廣泛,比如 AR/VR/MR 中,利用視覺追蹤,來 ...
概述
Gaze Input & Tracking - 也就是視覺輸入和跟蹤,是一種和滑鼠/觸摸屏輸入非常不一樣的交互方式,利用人類眼球的識別和眼球方向角度的跟蹤,來判斷人眼的目標和意圖,從而非常方便的完成對設備的控制和操作。這種交互方式,應用場景非常廣泛,比如 AR/VR/MR 中,利用視覺追蹤,來判斷 Reaility 中場景物體的方向和展示;再比如閱讀中,根據視覺輸入和追蹤,來自動滾動和翻頁等;再比如游戲中依靠視覺追蹤來決定人物的走位等,讓游戲控制變得非常簡單。
Windows 10 秋季創意者更新公佈了對視覺追蹤的原生支持,而在 Windows 10 四月更新中為開發者增加了 Windows Gaze Input API 來支持視覺追蹤開發,讓開發者可以在應用中加入視覺追蹤的交互方式來處理視覺輸入和跟蹤。
而在 Windows Community Toolkit 3.0 中,也加入了 Gaze Interaction Library,它基於 Windows Gaze Input API 創建,提供了一系列的開發者幫助類,幫助開發者可以更容易的實現對用戶視覺的追蹤。它旨在把通過 Windows API 來處理眼球追蹤的原始數據流的負責過程封裝處理,讓開發者可以更方便的在 Windows App 中集成。
下麵是 Windows Community Toolkit Sample App 的示例截圖和 code/doc 地址:
Windows Community Toolkit Doc - Gaze Interaction
Windows Community Toolkit Source Code - Gaze Interaction
Namespace: Microsoft.Toolkit.Uwp.Input.GazeInteraction; Nuget: Microsoft.Toolkit.Uwp.Input.GazeInteraction;
開發過程
代碼結構分析
首先來看 GazeInteraction 的代碼結構,通過類的命名可以看出,開發語言使用的是 C++,而且類結構和數量都比較複雜。可以看到 GazeInteraction 的代碼在 Microsoft.Toolkit.Uwp.Input namespace 下,這也意味著 GazeInteraction 會被作為一種 Input 方式來做處理。
來看一下在 Visual Studio 中打開的目錄,會更清晰一些:
因為是 C++ 語言編寫的庫,所以可以很清楚的看到,主要功能被劃分在 Headers 和 Sources 中,Headers 中主要是 cpp 對應的頭文件,以及一些枚舉類,變數定義類;Sources 中就是整個 GazeInteraction 的主要代碼處理邏輯;
我們挑選其中比較重要的幾個類來講解:
- GazeInput.cpp - Gaze 輸入的主要處理邏輯
- GazePointer.cpp - Gaze 指針的主要處理邏輯
- GazePointerProxy.cpp - Gaze 指針的代理處理邏輯
- GazeTargetItem.cpp - Gaze 操作目標的主要處理邏輯
1. GazeInput.cpp
在 GazeInput.h 中可以看到,定義了很多 public 的依賴屬性,主要針對的是 GazeInput 的游標屬性,以及很多 get/set 方法,以及 propertychanged 通知事件。
GazeInput 中定義的依賴屬性有:
- Interaction - 獲取和設置視覺交互屬性,它有三個枚舉值:Enabled/Disabled/Inherited;
- IsCursorVisible - 視覺交互的游標是否顯示,布爾值,預設為 false;
- CursorRadius - 獲取和設置視覺游標的半徑;
- GazeElement - 視覺元素,附加到控制項的代理對象允許訂閱每個視覺事件;
- FixationDuration - 獲取和設置從 Enter 狀態到 Fixation 狀態的轉換所需時間跨度,當 StateChanged 時間被觸發,PointerState 被設置為 Fixation,單位是 ms,預設為 350 ms;
- DwellDuration - 獲取和設置從 Fixation 狀態到 DWell 狀態的轉換所需時間跨度,當 StateChanged 時間被觸發,PointerState 被設置為 DWell,單位是 ms,預設為 400 ms;
- RepeatDelayDuration - 獲取和設置第一次重覆發生的持續時間,可以防止無意的重覆調用;
- DwellRepeatDuration - 獲取和設置 Dwell 重覆駐留調用的持續時間;
- ThresholdDuration - 獲取和設置從 Enter 狀態到 Exit 狀態的轉換所需時間跨度,當 StateChanged 時間被觸發,PointerState 被設置為 Exit,單位是 ms,預設為 50 ms;
- MaxDwellRepeatCount - 控制項重覆調用的最大次數,用戶的視覺不需要離開並重新進入控制項。預設值為 0,禁用重覆調用,開發者可以設置為 >0 的值來啟用重覆調用;
- IsSwitchEnabled - 標識切換是否可用,布爾值;
這些屬性的定義讓視覺輸入可以作為一種輸入方式,實現對系統界面元素的操作。
2. GazePointer.cpp
GazePointer 類主要處理的是 GazeInput 的定位和相關功能,代碼量比較大,不過每個方法功能都比較容易懂,我們通過幾個方法來看一些重要信息:
1). GazePointer 構造方法,看到方法中初始化了 NullFilter 和 GazeCursor,還定義了一段時間接收不到視覺輸入的定時處理,以及觀察器;
GazePointer::GazePointer() { _nonInvokeGazeTargetItem = ref new NonInvokeGazeTargetItem(); // Default to not filtering sample data Filter = ref new NullFilter(); _gazeCursor = ref new GazeCursor(); // timer that gets called back if there gaze samples haven't been received in a while _eyesOffTimer = ref new DispatcherTimer(); _eyesOffTimer->Tick += ref new EventHandler<Object^>(this, &GazePointer::OnEyesOff); // provide a default of GAZE_IDLE_TIME microseconds to fire eyes off EyesOffDelay = GAZE_IDLE_TIME; InitializeHistogram(); _watcher = GazeInputSourcePreview::CreateWatcher(); _watcher->Added += ref new TypedEventHandler<GazeDeviceWatcherPreview^, GazeDeviceWatcherAddedPreviewEventArgs^>(this, &GazePointer::OnDeviceAdded); _watcher->Removed += ref new TypedEventHandler<GazeDeviceWatcherPreview^, GazeDeviceWatcherRemovedPreviewEventArgs^>(this, &GazePointer::OnDeviceRemoved); _watcher->Start(); }
2). GetProperty 方法,這裡我們主要看看 PointerState,主要有 Fixation/DWell/DWellRepeat/Enter 和 Exit;
static DependencyProperty^ GetProperty(PointerState state) { switch (state) { case PointerState::Fixation: return GazeInput::FixationDurationProperty; case PointerState::Dwell: return GazeInput::DwellDurationProperty; case PointerState::DwellRepeat: return GazeInput::DwellRepeatDurationProperty; case PointerState::Enter: return GazeInput::ThresholdDurationProperty; case PointerState::Exit: return GazeInput::ThresholdDurationProperty; default: return nullptr; } }
3). GetElementStateDelay 方法,因為 GazePointer 有很多不同的狀態,我們看一個典型的獲取某個 state delay 的邏輯;根據用戶設置或預設設置的值,再根據 pointer state 和是否 repeat 來判斷 ticks 的值;
TimeSpan GazePointer::GetElementStateDelay(UIElement ^element, PointerState pointerState) { auto property = GetProperty(pointerState); auto defaultValue = GetDefaultPropertyValue(pointerState); auto ticks = GetElementStateDelay(element, property, defaultValue); switch (pointerState) { case PointerState::Dwell: case PointerState::DwellRepeat: _maxHistoryTime = max(_maxHistoryTime, 2 * ticks); break; } return ticks; }
TimeSpan GazePointer::GetElementStateDelay(UIElement ^element, DependencyProperty^ property, TimeSpan defaultValue) { UIElement^ walker = element; Object^ valueAtWalker = walker->GetValue(property); while (GazeInput::UnsetTimeSpan.Equals(valueAtWalker) && walker != nullptr) { walker = GetInheritenceParent(walker); if (walker != nullptr) { valueAtWalker = walker->GetValue(property); } } auto ticks = GazeInput::UnsetTimeSpan.Equals(valueAtWalker) ? defaultValue : safe_cast<TimeSpan>(valueAtWalker); return ticks; }
4). GetHitTarget 方法,獲取擊中的目標,根據指針的位置,和每個 target 在視覺樹中的位置,以及層級關係,來判斷該次擊中是否可用,應該產生什麼後續事件;
GazeTargetItem^ GazePointer::GetHitTarget(Point gazePoint) { GazeTargetItem^ invokable; switch (Window::Current->CoreWindow->ActivationMode) { default: invokable = _nonInvokeGazeTargetItem; break; case CoreWindowActivationMode::ActivatedInForeground: case CoreWindowActivationMode::ActivatedNotForeground: auto elements = VisualTreeHelper::FindElementsInHostCoordinates(gazePoint, nullptr, false); auto first = elements->First(); auto element = first->HasCurrent ? first->Current : nullptr; invokable = nullptr; if (element != nullptr) { invokable = GazeTargetItem::GetOrCreate(element); while (element != nullptr && !invokable->IsInvokable) { element = dynamic_cast<UIElement^>(VisualTreeHelper::GetParent(element)); if (element != nullptr) { invokable = GazeTargetItem::GetOrCreate(element); } } } ...break; } return invokable; }
GazePointer 類中處理方法非常多,這裡不一一列舉,大家可以詳細閱讀源代碼去理解每一個方法的書寫方法。
3. GazePointerProxy.cpp
GazePointerProxy 類主要是為 GazePointer 設立的代理,包括 Loaded 和 UnLoaded 事件的代理,以及 Enable 狀態和處理的代理;比較典型的 OnLoaded 事件處理:
void GazePointerProxy::OnLoaded(Object^ sender, RoutedEventArgs^ args) { assert(IsLoadedHeuristic(safe_cast<FrameworkElement^>(sender))); if (!_isLoaded) { // Record that we are now loaded. _isLoaded = true; // If we were previously enabled... if (_isEnabled) { // ...we can now be counted as actively enabled. GazePointer::Instance->AddRoot(sender); } } else { Debug::WriteLine(L"Unexpected Load"); } }
4. GazeTargetItem.cpp
Gaze 視覺輸入的 Target Item 類,針對不同類型的 Target,進行不同的交互和邏輯處理,比較典型的 PivotItemGazeTargetItem 類,會根據 PivotItem 的組成:headerItem 和 headerPanel,設置選中的 Index;
ref class PivotItemGazeTargetItem sealed : GazeTargetItem { internal: PivotItemGazeTargetItem(UIElement^ element) : GazeTargetItem(element) { } void Invoke() override { auto headerItem = safe_cast<PivotHeaderItem^>(TargetElement); auto headerPanel = safe_cast<PivotHeaderPanel^>(VisualTreeHelper::GetParent(headerItem)); unsigned index; headerPanel->Children->IndexOf(headerItem, &index); DependencyObject^ walker = headerPanel; Pivot^ pivot; do { walker = VisualTreeHelper::GetParent(walker); pivot = dynamic_cast<Pivot^>(walker); } while (pivot == nullptr); pivot->SelectedIndex = index; } };
調用示例
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" xmlns:g="using:Microsoft.Toolkit.Uwp.Input.GazeInteraction" g:GazeInput.Interaction="Enabled" g:GazeInput.IsCursorVisible="True" g:GazeInput.CursorRadius="5"> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Button x:Name="TargetButton" HorizontalAlignment="Center" BorderBrush="#7FFFFFFF" g:GazeInput.ThresholdDuration="00:00:00.0500000" g:GazeInput.FixationDuration="00:00:00.3500000" g:GazeInput.DwellDuration="00:00:00.4000000" g:GazeInput.RepeatDelayDuration="00:00:00.4000000" g:GazeInput.DwellRepeatDuration="00:00:00.4000000" g:GazeInput.MaxDwellRepeatCount="0" Width="100" Height="100" /> </Grid> </Page>
private void GazeButtonControl_StateChanged(object sender, GazePointerEventArgs ea) { if (ea.PointerState == GazePointerState.Enter) { } if (ea.PointerState == GazePointerState.Fixation) { } if (ea.PointerState == GazePointerState.Dwell) { if (dwellCount == 0) { dwellCount = 1; } else { dwellCount += 1; } } if (ea.PointerState == GazePointerState.Exit) { } } // You can respond to dwell progress in the ProgressFeedback handler private void OnProgressFeedback(object sender, GazeProgressEventArgs e){}private void OnGazeInvoked(object sender, GazeInvokedRoutedEventArgs e){}
總結
到這裡我們就把 Windows Community Toolkit 3.0 中的 Gaze Interation 的源代碼實現過程講解完成了,希望能對大家更好的理解和使用這個功能有所幫助。同時這一功能,對於開發 AR/VR/MR 和基於其他視覺追蹤設備的應用,會非常有想象空間,希望大家能有很多很好玩的想法,也歡迎和我們交流。
最後,再跟大家安利一下 WindowsCommunityToolkit 的官方微博:https://weibo.com/u/6506046490, 大家可以通過微博關註最新動態。
衷心感謝 WindowsCommunityToolkit 的作者們傑出的工作,感謝每一位貢獻者,Thank you so much, ALL WindowsCommunityToolkit AUTHORS !!!