Windows Community Toolkit 3.0 - Gaze Interaction

来源:https://www.cnblogs.com/shaomeng/archive/2018/08/05/9281073.html
-Advertisement-
Play Games

概述 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 !!!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • TOTP 的全稱是"基於時間的一次性密碼"(Time based One time Password)。它是公認的可靠解決方案,已經寫入國際標準 RFC6238。 很早就知道有這個東西了,一直不知道是怎麼實現的. 比如 QQ 安全中心的密鑰,U盾,就是動態密碼之類的. 今天看到阮一峰老師的博客才知道 ...
  • 一、Python編譯器簡介 根據實現Python編譯器語言一般分為以下幾種: 1.1、CPython 標準的Python,解釋型編譯器。 Python:標準的CPython版本,即官方發佈版本。 IPython:基於CPython的一個互動式解釋器,也就是說,IPython只是在交互方式上有所增強, ...
  • Hello Python3 數據類型以及傳參 內置函數 正則表達式 文件操作 異常處理 類和對象 MySQL操作 Socket編程 線程 HTTP請求 JSON 日期時間 感想 解決問題的思路才是關鍵,編程語言只是語法不同而已。 習慣性以分號結尾;哈哈哈!!! ...
  • .net core使用配置文件 在 .net core中,配置文件的讀取是通過IConfiguration來提供的,程式集是 ,對應的有一系列的實現,通過這些實現,可以讀取Json/Xml/ini等類型的配置文件。 在本節示例中,我們使用Json配置文件做演示。 讀取Json配置文件 Json是我們 ...
  • [TOC] C 編程指南 前不久在 Github 上看見了一位大牛創建一個倉庫: "CSharpCodingGuidelines" ,打開之後看了一下 相關描述,感覺應該很不錯,於是就 clone 到本地拜讀一下,這裡列一些自己的筆記,方便日後回顧。 基本原則 Astonishment 原則:你的代 ...
  • 今天我準備記錄一篇關於遍歷的博客,因為覺得它是我們以後工作最常用的一種方法了。比如說在一個模塊里插入小圖標,如京東網頁右側的小圖標<i></i>。 精靈圖中遍歷也是不可或缺的重要用法。 遍歷又是迴圈中最常見的問題。 所謂遍歷,是指有某個範圍的樣本數,需要把樣本中的每個數據取出來一一分析。 比如,輸出 ...
  • 今天試了下mvc自帶的ajax,發現上傳文件時後端action接收不到文件, Request.Files和HttpPostedFileBase都接收不到。。。。。後來搜索了下才知道mvc自帶的Ajax不支持文件上傳,無奈之下只能用其他的方式 第一種方式:通過 jquery的ajaxSubmit 》( ...
  • cSharp_1_概述 名詞描述 C# 是一門語言,語法與javascript、C、C++、java相近,這些語言都是比C語言的語系中發展而來。 .net framework (Framework是框架的意思)asp.net軟體的編譯和運行平臺,電腦必須安裝了這個軟體才可以運行我們編寫的C#應用程 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...