デコシノニッキ

ホロレンジャーの戦いの記録

SpectatorViewのQRCode Tracker の中身を見る

SpectaorViewのリポジトリに使われているQRTrackerが気になったので調べてみました。調べただけです(理由は後述)

github.com

位置合わせ

SpectatorView・・・というよりSharingや建築物など現実の環境で位置を合わせる必要があるものその全てに言えることですが、初期位置をどのように決めるのかというのはARアプリ開発においてネックになります。

blog.d-yama7.com

SpectatorViewは乱暴な言い方をすれば、イベントや位置情報をUDPなりUNET(あいつはもう死んだ)なりで共有するSharingとカメラ映像を補正してUnityEditor上で映す機能を組み合わせたものです。 なので、当然別途HoloLens、Viewer間での位置合わせの仕組みが必要です。

そこでSpectaorViewの位置合わせ方法では、

  • ArUco Marker
  • QR Code
  • Azure Spatial Anchors

の3つが提供されています。Azure Spatial Anchorsそのものは初期位置を決める仕組みではなく、Anchorを共有する仕組みですのでここでは割愛します。仕組みはこちら
マーカーを使った位置合わせは先のリンクで解説があるように大変有用です。しかしながら、サポートプラットフォームは下記の通りです。

f:id:haikage1755:20190710011416p:plain:w450

ArUcoはHL2非対称で、QRコードはHL1非対応です。今後を考えるなら、QRコード一択でしょう。
で、先にお断りしておきますが、調べただけの理由その1はHL2がないこと、その2は僕がスマホの開発環境を持っていないことです。

準備

  1. ドキュメントに従って作業します。こちらからPluginをDLします。

  2. Unityの設定、 Build Settings -> Player Settings -> Other Settings -> 'Scripting Defined Symbols' でQRCODESTRACKER_BINARY_AVAILABLEを指定します。

Noteにごちゃっと書いてありますが、これはHoloLens1でビルドするときは「QRCODESTRACKER_BINARY_AVAILABLEを削除してね」と書いてあります。QRCODESTRACKER_BINARY_AVAILABLEはQRトラックできることをコンパイラに教えるためのもので、HL1では前述の通りできないので削除します。

API

pluginの配布元のドキュメントが役に立ちます。

QRTrackingAPIが提供するAPI一覧です。

  • QRCode
  • EventArgs
    • QRCodeAddedEventArgs
    • QRCodeRemovedEventArgs
    • QRCodeUpdatedEventArgs
  • EventHandlers
    • QRCodeAddedHandler
    • QRCodeRemovedHandler
    • QRCodeUpdatedHandler
  • QRTrackerStartResult
  • QRTracker

重要なのはQRCode, QRTrackerです。

QRCode

QRCodeは文字通り、QRコードを表現するクラスです。QRCodeを一意に識別できるようGUIDが振られるようになっています。
その他にもどんな情報をもっているのかをざっくりまとめました。

バージョン: QRコードセル構成のことのようです。
PsysicalSizeMeters: QRコードの物理的な大きさ、これはQRコードとカメラ間距離を疑似的に算出するために設けられていると思われます。
Code: QRコードに埋め込まれている文字情報です。
CodeStream: 誤り訂正能力に使われるデータです。
LastDetectedQPCTicks : QPCはQueryPerformanceCounterのことで最後に検出した時のカウンタ値です。

    // Encapsulates information about a labeled QR code element.
    public class QRCode
    {        
        // Unique id that identifies this QR code for this session.
        public Guid Id { get; private set; }
           
        // Version of this QR code.
        public Int32 Version { get; private set; }
        
        // PhysicalSizeMeters of this QR code.
        public float PhysicalSizeMeters { get; private set; }
        
        // QR code Data.
        public string Code { get; private set; }
        
        // QR code DataStream this is the error corrected data stream
        public Byte[] CodeStream { get; private set; }
        
        // QR code last detected QPC ticks.
        public Int64 LastDetectedQPCTicks { get; private set; }
    };

QRTracker

QR Trackerは文字通りトラッキング機能を提供するAPIです。
ただ、機能といっても基本的にはQRコードの追加、更新、削除イベントを登録して、トラッキングを開始する…というシンプルなものですし、イベントに含まれるデータも追加、更新、削除が行われたQRCodeのみです。

        // Constructs a new QRTracker.
        public QRTracker(){}
        
        // Gets the QRTracker.
        public static QRTracker Get()
        {
            return new QRTracker();
        }
        
        // Start the QRTracker. Returns QRTrackerStartResult.
        public QRTrackerStartResult Start()
        {
            throw new NotImplementedException();
        }
        
        // Stops tracking QR codes.
        public void Stop() {}

        // Not Implemented
        Windows.Foundation.Collections.IVector<QRCode> GetList() { throw new NotImplementedException(); }
        
        // Event representing the addition of a QR Code.
        public event QRCodeAddedHandler Added = delegate { };
        
        // Event representing the removal of a QR Code.
        public event QRCodeRemovedHandler Removed = delegate { };
        
        // Event representing the update of a QR Code.
        public event QRCodeUpdatedHandler Updated = delegate { };
    };

QRTrackerをUnityで使う

QRCodeの検出それ自体はいいのですが、3D空間で扱うとなるとこのプラグインをそのまま使うというのは厳しいです。
そこでSpectatorViewから必要な部分を部分をぶっこ抜きます。

  • IMarkerDetector
  • IMarkerVisual
  • Marker
  • MarkerPositionBehaviour
  • QRCodeMarkerDetector
  • QRCodeMarkerVisual
  • QRCodeManager

肝になるのはQRCodeManagerです。
依存の向きはQRCodeManager<-QRCodeDetectorになります。

QRCodeManager

こちらはこれ単体で動作します。QRCodeManagerの役割はQRTrackerを管理し、QRCodeの検出、追加や削除の通知や座標の変換を行うヘルパーメソッドを提供しています。
使い方はStartQRTracking/StopQRTrackingで制御し、QRCodeAdded/QRCodeUpdated/QRCodeRemovedでQRCodeの変更通知を受け取り、受け取った側はTryGetLocationForQRCodeで座標変換を行うというのが主な流れです。

TryGetLocationForQRCodeの引数には,QRコードの座標系を渡します。
それをUnity座標系から変換したワールドの基準となる座標系から相対座標を算出します。

            /// <summary>
            /// Tries to obtain the QRCode location in Unity Space.
            /// The position component of the location matrix will be at the top left of the QRCode
            /// The orientation of the location matrix will reflect the following axii:
            /// x axis: horizontal with the QRCode.
            /// y axis: positive direction down the QRCode.
            /// z axis: positive direction outward from the QRCode.
            /// /// Note: This function should be called from the main thread
            /// </summary>
            /// <param name="coordinateSystem">QRCode SpatialCoordinateSystem</param>
            /// <param name="location">Output location for the QRCode in Unity Space</param>
            /// <returns>returns true if the QRCode was located</returns>
            public bool TryGetLocationForQRCode(SpatialCoordinateSystem coordinateSystem, out Matrix4x4 location)
            {
                location = Matrix4x4.identity;
                if (coordinateSystem != null)
                {
                    try
                    {
                        var appSpatialCoordinateSystem = (SpatialCoordinateSystem)System.Runtime.InteropServices.Marshal.GetObjectForIUnknown(UnityEngine.XR.WSA.WorldManager.GetNativeISpatialCoordinateSystemPtr());
                        if (appSpatialCoordinateSystem != null)
                        {
                            // Get the relative transform from the unity origin
                            System.Numerics.Matrix4x4? relativePose = coordinateSystem.TryGetTransformTo(appSpatialCoordinateSystem);
                            if (relativePose != null)
                            {
                                System.Numerics.Matrix4x4 newMatrix = relativePose.Value;

                                // Platform coordinates are all right handed and unity uses left handed matrices. so we convert the matrix
                                // from rhs-rhs to lhs-lhs
                                // Convert from right to left coordinate system
                                newMatrix.M13 = -newMatrix.M13;
                                newMatrix.M23 = -newMatrix.M23;
                                newMatrix.M43 = -newMatrix.M43;

                                newMatrix.M31 = -newMatrix.M31;
                                newMatrix.M32 = -newMatrix.M32;
                                newMatrix.M34 = -newMatrix.M34;

                                System.Numerics.Vector3 winrtScale;
                                System.Numerics.Quaternion winrtRotation;
                                System.Numerics.Vector3 winrtTranslation;
                                System.Numerics.Matrix4x4.Decompose(newMatrix, out winrtScale, out winrtRotation, out winrtTranslation);

                                var translation = new Vector3(winrtTranslation.X, winrtTranslation.Y, winrtTranslation.Z);
                                var rotation = new Quaternion(winrtRotation.X, winrtRotation.Y, winrtRotation.Z, winrtRotation.W);
                                location = Matrix4x4.TRS(translation, rotation, Vector3.one);

                                return true;
                            }
                            else
                            {
                                Debug.LogWarning("QRCode location unknown or not yet available.");
                                return false;
                            }
                        }
                        else
                        {
                            Debug.LogWarning("Failed to obtain coordinate system for application");
                            return false;
                        }
                    }
                    catch(Exception e)
                    {
                        Debug.LogWarning($"Note: TryGetLocationForQRCode needs to be called from main thread: {e}");
                        return false;
                    }
                }
                else
                {
                    Debug.LogWarning("Failed to obtain coordinate system for QRCode");
                    return false;
                }
            }

QRCodeMarkerDetector

QRCodeManagerから送られてくるイベントを受け取り、加工するユースケース部です。今回はサンプルとして抜き出していますが、SpectaorViewで利用するためのいらない部品(インタフェースなど)も含まれているため、ここを参考に拡張したり処理を新しく組んだりするのが良いでしょう。 マーカーの検出にはプレフィックスが用いられており、QRCode.Codeつまりマーカーの情報にSpectatorViewを意味する"sv"を含むものしか検知しないようになっています。 このプレフィックスを削除したり、別の文字で置き換えてもいいでしょう。

        private bool TryGetMarkerId(string qrCode, out int markerId)
        {
            markerId = -1;
            if (qrCode != null &&
                qrCode.Trim().StartsWith(_qrCodeNamePrefix))
            {
                var qrCodeId = qrCode.Trim().Replace(_qrCodeNamePrefix, "");
                if (Int32.TryParse(qrCodeId, out markerId))
                {
                    return true;
                }
            }

            Debug.Log("Unable to obtain markerId for QR code: " + qrCode);
            markerId = -1;
            return false;
        }

QRマーカーが追加、削除、更新されるとProcessMarkerUpdatesが走り、マーカーが更新されたことが通知されます。
マーカーの座標計算処理には先で説明したCoodinateSystemを使った処理とUnityTransformへの変換の2プロセスが順に走ります。

private void ProcessMarkerUpdates()
        {
            bool locatedAllMarkers = true;
            var markerDictionary = new Dictionary<int, Marker>();
            lock (_contentLock)
            {
                foreach (var markerPair in _markerIds)
                {
                    if (!_markerCoordinateSystems.ContainsKey(markerPair.Key))
                    {
                        var coordinateSystem = SpatialGraphInteropPreview.CreateCoordinateSystemForNode(markerPair.Key);
                        if (coordinateSystem != null)
                        {
                            _markerCoordinateSystems[markerPair.Key] = coordinateSystem;
                        }
                    }
                }

                foreach (var coordinatePair in _markerCoordinateSystems)
                {
                    if (!_markerIds.TryGetValue(coordinatePair.Key, out var markerId))
                    {
                        Debug.Log($"Failed to locate marker:{coordinatePair.Key}, {markerId}");
                        locatedAllMarkers = false;
                        continue;
                    }

                    if (_qrCodesManager.TryGetLocationForQRCode(coordinatePair.Key, out var location))
                    {
                        var translation = location.GetColumn(3);
                        // The obtained QRCode orientation will reflect a positive y axis down the QRCode.
                        // Spectator view marker detectors should return a positive y axis up the marker,
                        // so, we rotate the marker orientation 180 degrees around its z axis.
                        var rotation = Quaternion.LookRotation(location.GetColumn(2), location.GetColumn(1)) * Quaternion.Euler(0, 0, 180);

                        if (_markerSizes.TryGetValue(markerId, out var size))
                        {
                            var transform = Matrix4x4.TRS(translation, rotation, Vector3.one);
                            var offset = -1.0f * size / 2.0f;
                            var markerCenter = transform.MultiplyPoint(new Vector3(offset, offset, 0));
                            var marker = new Marker(markerId, markerCenter, rotation);
                            markerDictionary[markerId] = marker;
                        }
                    }
                }
            }

            if (markerDictionary.Count > 0 || locatedAllMarkers)
            {
                MarkersUpdated?.Invoke(markerDictionary);
            }

            // Stop processing markers once all markers have been located
            _processMarkers = !locatedAllMarkers;
        }

Unity向け処理では右手系で処理されるCoordinateSystemの値を,Unityに合わせて左手系に変換する処理を行っています。
右手系だとY値が下に来るのでZ軸を中心に回転処理を加えています。
加えて,マーカーは左上が中心となるのでマーカーサイズの半分をオフセットとして与えて中心の補正を行っています。

f:id:haikage1755:20190710114057p:plain:w250

最終的に検出したマーカーはMarkersUpdatedHandler経由で取得可能です。

以上がQRCode Trackerの使い方の流れと大まかな仕組みです。

まとめ

QR CodeがHLでも使えるようになることで,Vuforiaに依存しないアプリケーションの構築ができるというのが(お値段的な意味で)最大のメリットだと思います。
現状業務用途でも,リアルタイムでトラッキングを要するものは少なく,如何に初期位置を合わせるかの方が重要な印象です。またQRコードそれ自体に自由に情報を埋め込めるため,幅広く使えるのもポイントです。

なぜHoloLens1に対応していないのか

実はこのQRMarkerのこの仕組み自体は別段新しいものではなく昨年の時点で公開されている上,Immersiveでも動きます。

ただし、ドキュメントにもあるようにレジストリの書き換えがRS5の時点では必要でした。これはHoloLensではできない処理です。
19H1からは、このレジストリの書き換えが必要なくなりました…が、19H1はなんと現時点でもHoloLensには降ってきてないのです。恐らく2が出る上、開発機的な立ち位置だった初代は更新を止め、修正のみ対応のフェーズに入るんじゃないかなぁという予想。Slackで本社の人にも訪ねてたみたのですが華麗にスルーされました。残念。
ワンチャンRS4のように時間差大型アップデートはあるかもしれないですが、限りなく現実的ではないですかねぇ。企業ユーザ数も少なくはないのでいきなりのサポートぶっちはないと信じたい。公式アナウンス待ちですね。

QRコードが読みたい

人力でもいけるらしいです。弊社にもペイントでQRコード書いている人がいました。(どうでもいい) nlab.itmedia.co.jp

[デコシノニッキ]は、Amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、Amazonアソシエイト・プログラムの参加者です。」