ネイティブ エンジンでの拡張視線追跡

拡張視線追跡は、HoloLens 2の新機能です。 これは、組み合わされた視線入力データのみを提供する、標準的な視線追跡のスーパーセットです。 拡張視線追跡では、個々の視線入力データも提供され、アプリケーションは、30、60、90fps などの視線データに対して異なるフレームレートを設定できます。 目の開きや目の色などの他の機能は、現時点ではHoloLens 2ではサポートされていません。

Extended Eye Tracking SDK を使用すると、アプリケーションは拡張視線追跡のデータと機能にアクセスできます。 WinRT API または OpenXR API と共に使用できます。

この記事では、拡張視線追跡 SDK をネイティブ エンジン (C# または C++/WinRT) で WinRT API と共に使用する方法について説明します。

プロジェクトの設定

  1. Holographic DirectX 11 App (Universal Windows) Visual Studio 2019 以降で または Holographic DirectX 11 App (Universal Windows) (C++/WinRT) プロジェクトを作成するか、既存のホログラフィック Visual Studio プロジェクトを開きます。
  2. 拡張視線追跡 SDK をプロジェクトにインポートします。
    1. Visual Studio ソリューション エクスプローラーで、プロジェクトを右クリックします -> NuGet パッケージの管理...
    2. 右上隅にある [パッケージ ソース] が次の nuget.org をポイントしていることを確認します。 https://api.nuget.org/v3/index.json
    3. [ブラウザー] タブをクリックし、 を検索 Microsoft.MixedReality.EyeTrackingします。
    4. [インストール] ボタンをクリックして、最新バージョンの SDK をインストールします。
      Eye Tracking SDK Nuget パッケージのスクリーンショット。
  3. 視線入力機能を設定する
    1. ソリューション エクスプローラーの Package.appxmanifest ファイルをダブルクリックします。
    2. [機能] タブクリックし、[視線入力] をチェックします。
  4. ヘッド ファイルを含め、名前空間を使用します。
    • C# プロジェクトの場合:
    using Microsoft.MixedReality.EyeTracking;
    
    • C++/WinRT プロジェクトの場合:
    #include <winrt/Microsoft.MixedReality.EyeTracking.h>
    using namespace winrt::Microsoft::MixedReality::EyeTracking;
    
  5. 拡張視線追跡 SDK API を使用し、ロジックを実装します。
  6. HoloLens をビルドしてデプロイします

視線入力データを取得する手順の概要

拡張視線追跡 SDK API を使用して視線入力データを取得するには、次の手順が必要です。

  1. ユーザーから視線追跡機能へのアクセス権を取得します。
  2. 目の視線入力トラッカーの接続と切断を監視します。
  3. 目の視線トラッカーを開き、その機能に対してクエリを実行します。
  4. 視線追跡ツールから視線データを繰り返し読み取る。
  5. 視線入力データを他の SpatialCoordinateSystems に転送します。

視線追跡機能にアクセスする

目に関連する情報を使用するには、アプリケーションが最初にユーザーの同意を要求する必要があります。

var status = await Windows.Perception.People.EyesPose.RequestAccessAsync();
bool useGaze = (status == Windows.UI.Input.GazeInputAccessStatus.Allowed);
auto accessStatus = co_await winrt::Windows::Perception::People::EyesPose::RequestAccessAsync();
bool useGaze = (accessStatus.get() == winrt::Windows::UI::Input::GazeInputAccessStatus::Allowed);

視線追跡ツールを検出する

視線追跡ツールの検出は、 クラスを使用 EyeGazeTrackerWatcher して行われます。 EyeGazeTrackerAdded および EyeGazeTrackerRemoved イベントは、視線追跡ツールが検出または切断されると、それぞれ発生します。

ウォッチャーは、 メソッドを使用して StartAsync() 明示的に開始する必要があります。これは、既に接続されているトラッカーがイベントを介して通知されたときに非同期的に EyeGazeTrackerAdded 完了します。

視線入力トラッカーが検出されると、 EyeGazeTracker イベント パラメーターで EyeGazeTrackerAdded インスタンスがアプリケーションに渡されます。再循環的に、トラッカーが切断されると、対応する EyeGazeTracker インスタンスが EyeGazeTrackerRemoved イベントに渡されます。

EyeGazeTrackerWatcher watcher = new EyeGazeTrackerWatcher();
watcher.EyeGazeTrackerAdded += _watcher_EyeGazeTrackerAdded;
watcher.EyeGazeTrackerRemoved += _watcher_EyeGazeTrackerRemoved;
await watcher.StartAsync();
...

private async void _watcher_EyeGazeTrackerAdded(object sender, EyeGazeTracker e)
{
    // Implementation is in next section
}

private void _watcher_EyeGazeTrackerRemoved(object sender, EyeGazeTracker e)
{
    ...
}
EyeGazeTrackerWatcher watcher;
watcher.EyeGazeTrackerAdded(std::bind(&SampleEyeTrackingNugetClientAppMain::OnEyeGazeTrackerAdded, this, _1, _2));
watcher.EyeGazeTrackerRemoved(std::bind(&SampleEyeTrackingNugetClientAppMain::OnEyeGazeTrackerRemoved, this, _1, _2));
co_await watcher.StartAsync();
...

winrt::Windows::Foundation::IAsyncAction SampleAppMain::OnEyeGazeTrackerAdded(const EyeGazeTrackerWatcher& sender, const EyeGazeTracker& tracker)
{
    // Implementation is in next section
}
void SampleAppMain::OnEyeGazeTrackerRemoved(const EyeGazeTrackerWatcher& sender, const EyeGazeTracker& tracker)
{
    ...
}

目を開く視線トラッカー

インスタンスを EyeGazeTracker 受け取る場合、アプリケーションは最初に メソッドを呼び出してインスタンスを OpenAsync() 開く必要があります。 その後、必要に応じてトラッカー機能のクエリを実行できます。 メソッドは OpenAsync() ブール値パラメーターを受け取ります。これは、個々の視線入力ベクトルやトラッカーのフレームレートの変更など、標準の視線追跡に属さない機能にアプリケーションがアクセスする必要があるかどうかを示します。

結合された視線入力は、すべての視線入力トラッカーでサポートされている必須の機能です。 個々の視線入力へのアクセスなど、その他の機能は省略可能であり、トラッカーとそのドライバーによってはサポートされる場合とサポートされていない場合があります。 これらのオプション機能の場合、 クラスは、 EyeGazeTracker 機能がサポートされているかどうかを示すプロパティを公開します。たとえば、 AreLeftAndRightGazesSupported プロパティは、個々の視線入力情報がデバイスでサポートされているかどうかを示します。

視線視線トラッカーによって公開されるすべての空間情報は、 動的ノード ID によって識別されるトラッカー自体に関連して公開されます。 nodeId を使用して WinRT API を使用して を SpatialCoordinateSystem 取得すると、視線データの座標が別の座標系に変換される可能性があります。

private async void _watcher_EyeGazeTrackerAdded(object sender, EyeGazeTracker e)
{
    try
    {
        // Try to open the tracker with access to restricted features
        await e.OpenAsync(true);

        // If it has succeeded, store it for future use
        _tracker = e;

        // Check support for individual eye gaze
        bool supportsIndividualEyeGaze = _tracker.AreLeftAndRightGazesSupported;

        // Get a spatial locator for the tracker, this will be used to transfer the gaze data to other coordinate systems later
        var trackerNodeId = e.TrackerSpaceLocatorNodeId;
        _trackerLocator = Windows.Perception.Spatial.Preview.SpatialGraphInteropPreview.CreateLocatorForNode(trackerNodeId);
    }
    catch (Exception ex)
    {
        // Unable to open the tracker
    }
}
winrt::Windows::Foundation::IAsyncAction SampleEyeTrackingNugetClientAppMain::OnEyeGazeTrackerAdded(const EyeGazeTrackerWatcher&, const EyeGazeTracker& tracker)
{
   auto newTracker = tracker;

   try
   {
        // Try to open the tracker with access to restricted features
        co_await newTracker.OpenAsync(true);

        // If it has succeeded, store it for future use
        m_gazeTracker = newTracker;

        // Check support for individual eye gaze
        const bool supportsIndividualEyeGaze = m_gazeTracker.AreLeftAndRightGazesSupported();

        // Get a spatial locator for the tracker. This will be used to transfer the gaze data to other coordinate systems later
        const auto trackerNodeId = m_gazeTracker.TrackerSpaceLocatorNodeId();
        m_trackerLocator = winrt::Windows::Perception::Spatial::Preview::SpatialGraphInteropPreview::CreateLocatorForNode(trackerNodeId);
   }
   catch (const winrt::hresult_error& e)
   {
       // Unable to open the tracker
   }
}

視線入力トラッカーのフレーム レートを設定する

プロパティは EyeGazeTracker.SupportedTargetFrameRates 、トラッカーでサポートされているターゲット フレーム レートの一覧を返します。 HoloLens 2では、30、60、90fps がサポートされます。

メソッドを EyeGazeTracker.SetTargetFrameRate() 使用して、ターゲット フレーム レートを設定します。

// This returns a list of supported frame rate: 30, 60, 90 fps in order
var supportedFrameRates = _tracker.SupportedTargetFrameRates;

// Sets the tracker at the highest supported frame rate (90 fps)
var newFrameRate = supportedFrameRates[supportedFrameRates.Count - 1];
_tracker.SetTargetFrameRate(newFrameRate);
uint newFramesPerSecond = newFrameRate.FramesPerSecond;
// This returns a list of supported frame rate: 30, 60, 90 fps in order
const auto supportedFrameRates = m_gazeTracker.SupportedTargetFrameRates();

// Sets the tracker at the highest supported frame rate (90 fps)
const auto newFrameRate = supportedFrameRates.GetAt(supportedFrameRates.Size() - 1);
m_gazeTracker.SetTargetFrameRate(newFrameRate);
const uint32_t newFramesPerSecond = newFrameRate.FramesPerSecond();

視線追跡ツールから視線データを読み取る

視線追跡ツールは、その状態を定期的に循環バッファーに発行します。 これにより、アプリケーションは、小さな期間に属する一度にトラッカーの状態を読み取ります。 これにより、たとえば、トラッカーの最新の状態や、ユーザーからの手のジェスチャなどのイベント時の状態を取得できます。

トラッカーの状態をインスタンスとして取得する EyeGazeTrackerReading メソッド:

  • メソッドと TryGetReadingAtSystemRelativeTime() メソッドはTryGetReadingAtTimestamp()、アプリケーションによってEyeGazeTrackerReading渡された時間に最も近い を返します。 トラッカーは発行スケジュールを制御するため、返される読み取りは要求時刻よりも少し古いか新しい可能性があります。 EyeGazeTrackerReading.Timestampプロパティと EyeGazeTrackerReading.SystemRelativeTime プロパティを使用すると、アプリケーションは発行済み状態の正確な時刻を把握できます。

  • メソッドと TryGetReadingAfterSystemRelativeTime() メソッドはTryGetReadingAfterTimestamp()、パラメーターとして渡された時間より厳密に優れたタイムスタンプを持つ最初EyeGazeTrackerReadingの を返します。 これにより、アプリケーションはトラッカーによって発行されたすべての状態を順番に読み取られます。 これらのメソッドはすべて既存のバッファーに対してクエリを実行しており、すぐに返されることに注意してください。 使用可能な状態がない場合は、null が返されます (つまり、アプリケーションは状態が公開されるまで待機しません)。

インスタンスにはIsCalibrationValidEyeGazeTrackerReadingタイムスタンプに加えて、アイ トラッカーの調整が有効かどうかを示す プロパティがあります。

最後に、視線入力データは、 や TryGetLeftEyeGazeInTrackerSpace()などのTryGetCombinedEyeGazeInTrackerSpace()一連のメソッドを使用して取得できます。 これらすべてのメソッドは、成功を示すブール値を返します。 一部のデータを取得できない場合は、データがサポートされていない (EyeGazeTracker このケースを検出するためのプロパティがある) か、トラッカーがデータを取得できなかった (無効な調整や目が非表示など) ことを意味する可能性があります。

たとえば、アプリケーションが結合された視線入力に対応するカーソルを表示する場合は、次のように準備されているフレームの予測のタイムスタンプを使用してトラッカーにクエリを実行できます。

var holographicFrame = holographicSpace.CreateNextFrame();
var prediction = holographicFrame.CurrentPrediction;
var predictionTimestamp = prediction.Timestamp;
var reading = _tracker.TryGetReadingAtTimestamp(predictionTimestamp.TargetTime.DateTime);
if (reading != null)
{
    // Vector3 needs the System.Numerics namespace
    if (reading.TryGetCombinedEyeGazeInTrackerSpace(out Vector3 gazeOrigin, out Vector3 gazeDirection))
    {
        // Use gazeOrigin and gazeDirection to display the cursor
    }
}
auto holographicFrame = m_holographicSpace.CreateNextFrame();
auto prediction = holographicFrame.CurrentPrediction();
auto predictionTimestamp = prediction.Timestamp();
const auto reading = m_gazeTracker.TryGetReadingAtTimestamp(predictionTimestamp.TargetTime());
if (reading)
{
    float3 gazeOrigin;
    float3 gazeDirection;
    if (reading.TryGetCombinedEyeGazeInTrackerSpace(gazeOrigin, gazeDirection))
    {
        // Use gazeOrigin and gazeDirection to display the cursor
    }
}

視線入力データを他の SpatialCoordinateSystem に変換する

位置などの空間データを返す WinRT API では、常に と の両方が PerceptionTimestamp 必要です SpatialCoordinateSystem。 たとえば、WinRT API を使用してHoloLens 2の視線入力を結合して取得するには、API SpatialPointerPose.TryGetAtTimestamp() に と の 2 つのパラメーター SpatialCoordinateSystemPerceptionTimestampが必要です。 結合された視線入力が を介して SpatialPointerPose.Eyes.Gazeアクセスされると、その原点と方向が渡された で SpatialCoordinateSystem 表されます。

拡張 tye 追跡 SDK API は を受け取る SpatialCoordinateSystem 必要がなく、視線入力データは常にトラッカーの座標系で表されます。 ただし、他の座標系に関連するトラッカーの姿勢を使用して、これらの視線入力データを別の座標系に変換できます。

  • 上記の「目を開く視線トラッカー」という名前のセクションで説明したように、視線視線トラッカーの をSpatialLocator取得するには、 プロパティを使用して をEyeGazeTracker.TrackerSpaceLocatorNodeId呼び出Windows.Perception.Spatial.Preview.SpatialGraphInteropPreview.CreateLocatorForNode()します。

  • 視線入力の原点と方向は EyeGazeTrackerReading 、視線追跡ツールに関連しています。

  • SpatialLocator.TryLocateAtTimestamp() は、特定の で、特定 PerceptionTimeStamp の に関連する目の視線トラッカーの完全な 6DoF 位置を SpatialCoordinateSystem返します。これは、Matrix4x4 変換行列を構築するために使用できます。

  • 視線の原点と方向を他の SpatialCoordinateSystem に転送するには、構築された Matrix4x4 変換マトリックスを使用します。

次のコード サンプルは、視線入力元の前の 2 メートルの、結合された視線入力の方向にあるキューブの位置を計算する方法を示しています。

var predictionTimestamp = prediction.Timestamp;
var stationaryCS = stationaryReferenceFrame.CoordinateSystem;
var trackerLocation = _trackerLocator.TryLocateAtTimestamp(predictionTimestamp, stationaryCS);
if (trackerLocation != null)
{
    var trackerToStationaryMatrix = Matrix4x4.CreateFromQuaternion(trackerLocation.Orientation) * Matrix4x4.CreateTranslation(trackerLocation.Position);
    var reading = _tracker.TryGetReadingAtTimestamp(predictionTimestamp.TargetTime.DateTime);
    if (reading != null)
    {
        if (reading.TryGetCombinedEyeGazeInTrackerSpace(out Vector3 gazeOriginInTrackerSpace, out Vector3 gazeDirectionInTrackerSpace))
        {
            var cubePositionInTrackerSpace = gazeOriginInTrackerSpace + 2.0f * gazeDirectionInTrackerSpace;
            var cubePositionInStationaryCS = Vector3.Transform(cubePositionInTrackerSpace, trackerToStationaryMatrix);
        }
    }
}
auto predictionTimestamp = prediction.Timestamp();
auto stationaryCS = m_stationaryReferenceFrame.CoordinateSystem();
auto trackerLocation = m_trackerLocator.TryLocateAtTimestamp(predictionTimestamp, stationaryCS);
if (trackerLocation) 
{
    auto trackerOrientation = trackerLocation.Orientation();
    auto trackerPosition = trackerLocation.Position();
    auto trackerToStationaryMatrix = DirectX::XMMatrixRotationQuaternion(DirectX::XMLoadFloat4(reinterpret_cast<const DirectX::XMFLOAT4*>(&trackerOrientation))) * DirectX::XMMatrixTranslationFromVector(DirectX::XMLoadFloat3(&trackerPosition));

    const auto reading = m_gazeTracker.TryGetReadingAtTimestamp(predictionTimestamp.TargetTime());
    if (reading)
    {
        float3 gazeOriginInTrackerSpace;
        float3 gazeDirectionInTrackerSpace;
        if (reading.TryGetCombinedEyeGazeInTrackerSpace(gazeOriginInTrackerSpace, gazeDirectionInTrackerSpace))
        {
            auto cubePositionInTrackerSpace = gazeOriginInTrackerSpace + 2.0f * gazeDirectionInTrackerSpace;
            float3 cubePositionInStationaryCS;
            DirectX::XMStoreFloat3(&cubePositionInStationaryCS, DirectX::XMVector3TransformCoord(DirectX::XMLoadFloat3(&cubePositionInTrackerSpace), trackerToStationaryMatrix));
        }
    }
}

拡張視線追跡 SDK の API リファレンス

namespace Microsoft.MixedReality.EyeTracking
{
    /// <summary>
    /// Allow discovery of Eye Gaze Trackers connected to the system
    /// This is the only class from Extended Eye Tracking SDK that the application will instantiate, 
    /// other classes' instances will be returned by method calls or properties.
    /// </summary>
    public class EyeGazeTrackerWatcher
    {
        /// <summary>
        /// Constructs an instance of the watcher
        /// </summary>
        public EyeGazeTrackerWatcher();

        /// <summary>
        /// Starts trackers enumeration.
        /// </summary>
        /// <returns>Task representing async action; completes when the initial enumeration is completed</returns>
        public System.Threading.Tasks.Task StartAsync();

        /// <summary>
        /// Stop listening to trackers additions and removal
        /// </summary>
        public void Stop();

        /// <summary>
        /// Raised when an Eye Gaze tracker is connected
        /// </summary>
        public event System.EventHandler<EyeGazeTracker> EyeGazeTrackerAdded;

        /// <summary>
        /// Raised when an Eye Gaze tracker is disconnected
        /// </summary>
        public event System.EventHandler<EyeGazeTracker> EyeGazeTrackerRemoved;        
    }

    /// <summary>
    /// Represents an Eye Tracker device
    /// </summary>
    public class EyeGazeTracker
    {
        /// <summary>
        /// True if Restricted mode is supported, which means the driver supports to provide individual 
        /// eye gaze vector and framerate 
        /// </summary>
        public bool IsRestrictedModeSupported;

        /// <summary>
        /// True if Vergence Distance is supported by tracker
        /// </summary>
        public bool IsVergenceDistanceSupported;

        /// <summary>
        /// True if Eye Openness is supported by the driver
        /// </summary>
        public bool IsEyeOpennessSupported;

        /// <summary>
        /// True if individual gazes are supported
        /// </summary>
        public bool AreLeftAndRightGazesSupported;

        /// <summary>
        /// Get the supported target frame rates of the tracker
        /// </summary>
        public System.Collections.Generic.IReadOnlyList<EyeGazeTrackerFrameRate> SupportedTargetFrameRates;

        /// <summary>
        /// NodeId of the tracker, used to retrieve a SpatialLocator or SpatialGraphNode to locate the tracker in the scene
        /// for Perception API, use SpatialGraphInteropPreview.CreateLocatorForNode
        /// for Mixed Reality OpenXR API, use SpatialGraphNode.FromDynamicNodeId
        /// </summary>
        public Guid TrackerSpaceLocatorNodeId;

        /// <summary>
        /// Opens the tracker
        /// </summary>
        /// <param name="restrictedMode">True if restricted mode active</param>
        /// <returns>Task representing async action; completes when the initial enumeration is completed</returns>
        public System.Threading.Tasks.Task OpenAsync(bool restrictedMode);

        /// <summary>
        /// Closes the tracker
        /// </summary>
        public void Close();

        /// <summary>
        /// Changes the target frame rate of the tracker
        /// </summary>
        /// <param name="newFrameRate">Target frame rate</param>
        public void SetTargetFrameRate(EyeGazeTrackerFrameRate newFrameRate);

        /// <summary>
        /// Try to get tracker state at a given timestamp
        /// </summary>
        /// <param name="timestamp">timestamp</param>
        /// <returns>State if available, null otherwise</returns>
        public EyeGazeTrackerReading TryGetReadingAtTimestamp(DateTime timestamp);

        /// <summary>
        /// Try to get tracker state at a system relative time
        /// </summary>
        /// <param name="time">time</param>
        /// <returns>State if available, null otherwise</returns>
        public EyeGazeTrackerReading TryGetReadingAtSystemRelativeTime(TimeSpan time);

        /// <summary>
        /// Try to get first first tracker state after a given timestamp
        /// </summary>
        /// <param name="timestamp">timestamp</param>
        /// <returns>State if available, null otherwise</returns>
        public EyeGazeTrackerReading TryGetReadingAfterTimestamp(DateTime timestamp);

        /// <summary>
        /// Try to get the first tracker state after a system relative time
        /// </summary>
        /// <param name="time">time</param>
        /// <returns>State if available, null otherwise</returns>
        public EyeGazeTrackerReading TryGetReadingAfterSystemRelativeTime(TimeSpan time);
    }

    /// <summary>
    /// Represents a Frame Rate supported by an Eye Tracker
    /// </summary>
    public class EyeGazeTrackerFrameRate
    {
        /// <summary>
        /// Frames per second of the frame rate
        /// </summary>
        public UInt32 FramesPerSecond;
    }

    /// <summary>
    /// Snapshot of Gaze Tracker state
    /// </summary>
    public class EyeGazeTrackerReading
    {
        /// <summary>
        /// Timestamp of state
        /// </summary>
        public DateTime Timestamp;

        /// <summary>
        /// Timestamp of state as system relative time
        /// Its SystemRelativeTime.Ticks could provide the QPC time to locate tracker pose 
        /// </summary>
        public TimeSpan SystemRelativeTime;

        /// <summary>
        /// Indicates user calibration is valid
        /// </summary>
        public bool IsCalibrationValid;

        /// <summary>
        /// Tries to get a vector representing the combined gaze related to the tracker's node
        /// </summary>
        /// <param name="origin">Origin of the gaze vector</param>
        /// <param name="direction">Direction of the gaze vector</param>
        /// <returns></returns>
        public bool TryGetCombinedEyeGazeInTrackerSpace(out System.Numerics.Vector3 origin, out System.Numerics.Vector3 direction);

        /// <summary>
        /// Tries to get a vector representing the left eye gaze related to the tracker's node
        /// </summary>
        /// <param name="origin">Origin of the gaze vector</param>
        /// <param name="direction">Direction of the gaze vector</param>
        /// <returns></returns>
        public bool TryGetLeftEyeGazeInTrackerSpace(out System.Numerics.Vector3 origin, out System.Numerics.Vector3 direction);

        /// <summary>
        /// Tries to get a vector representing the right eye gaze related to the tracker's node position
        /// </summary>
        /// <param name="origin">Origin of the gaze vector</param>
        /// <param name="direction">Direction of the gaze vector</param>
        /// <returns></returns>
        public bool TryGetRightEyeGazeInTrackerSpace(out System.Numerics.Vector3 origin, out System.Numerics.Vector3 direction);

        /// <summary>
        /// Tries to read vergence distance
        /// </summary>
        /// <param name="value">Vergence distance if available</param>
        /// <returns>bool if value is valid</returns>
        public bool TryGetVergenceDistance(out float value);

        /// <summary>
        /// Tries to get left Eye openness information
        /// </summary>
        /// <param name="value">Eye Openness if valid</param>
        /// <returns>bool if value is valid</returns>
        public bool TryGetLeftEyeOpenness(out float value);

        /// <summary>
        /// Tries to get right Eye openness information
        /// </summary>
        /// <param name="value">Eye Openness if valid</param>
        /// <returns>bool if value is valid</returns>
        public bool TryGetRightEyeOpenness(out float value);
    }
}

関連項目