なんかいろいろしてみます

Nov 24, 2017 - 3 minute read - HoloLens

HoloLensでSharingする方法(位置合わせ編)

HoloLensでSharing行う場合に必要な項目であるHoloLens同士の位置合わせについて書いています.

HoloLensのSharingのためにネットワーク通信の方法については TCP編UDP編 に書いたので今回は共通の空間を体験するために必要な位置合わせについてです.

Hololensによる位置合わせとは

HoloLensは本体に搭載されている環境認識カメラによって現実世界の形状を把握し自分の位置を推定しています. それによってUnityなどで現実空間に配置したオブジェクトはHoloLensが移動しても設置した場所にとどまることができます.

HoloLensのSharing機能はHoloLensが個々で認識しているオブジェクトを共有することで同一のHoloLens空間を体験することができるのです. しかしながらHoloLensが持つ自分の位置と空間内に設置したオブジェクトの位置情報はデバイス固有の物なのでそのまま位置データとして共有することはできません. 特にUnityを利用している場合にはアプリ起動時のHoloLensの位置を原点(0,0,0)とするため,HoloLensを別々の位置で起動しただけで位置が合わなくなってしまいます.

共通アンカー指定方法

複数のHoloLensはそれぞれ独自の位置情報を持っているためSharingするためには基準となる位置情報を共有する必要があります. 特にHoloLensでは現実空間が見えているため,Hololens同士の位置と表示オブジェクトの位置を揃えてあげる必要があります.

基準となる位置座標を指定するために大きく分けて以下の3点があります.

SharingWorldAnchor編

WorldAnchorはHoloLensが認識している現実世界の形状情報から設置したオブジェクトの現実空間内での位置を保存,復元する機能です. これを利用しHoloLens空間内にWorldAnchorを設置します. その後設置されたWorldAnchor周辺の形状情報をネットワークで他のHoloLensに共有し,受信したHoloLensは受け取った形状情報に最も近い位置にWorldAnchorを設置します. Shringを行うHoloLensが同一空間内にあれば取得される現実空間の形状情報は近くなるはずなので,結果的に現実空化の同一個所に基準座標であるWorldAnchorを設置できるようになります.

この方法はMixedRealityToolkitのShring機能で用いられている方法です. アプリ起動後は特に操作の必要もなく手軽にSharingによる体験が行えます.


ただしWorldAnchorを使う方法はShringを行うHoloLensがそれぞれ似た現実世界の形状情報を持っていることが情景になります. そのため以下のような問題が生じます.

  • HoloLensの位置によって形状情報が異なる
  • 人が動くと形状情報が変わる
  • 物が動くと形状情報が変わる
  • HoloLensの形状情報を大まかなので形状情報が変わる

またShringで他のHoloLensのWorldAnchor情報を再現する関係上以下の問題も発生します.

  • HoloLensのWorldAnchor情報を送信できない
  • HoloLensのWorldAnchor情報を受信できない
  • 受信したデータが壊れている
  • 受信したWorldAnchor情報と自信の形状情報が一致しない

Shringを行う環境によってはWorldAnchorの共有ができず位置合わせの成功率が大幅に低下する場合があります.

オブジェクト設置編

WorldAnchorは形状情報の共有が正確に行えないと位置合わせが失敗する場合があります. しかし個々のHoloLensは自身の位置情報は常に把握しているため,それぞれのHoloLensが基準座標を設定することで位置合わせが行えます. 現実空間の指定の物体にHoloLensでオブジェクトを重ねるようにして置き,そのオブジェクトを基準座標とする方法です. 現実空間の物体を移動させなければ他のHoloLensも同じ位置にオブジェクトを配置することができ結果的に基準座標の位置合わせができるようになります. この方法ではHoloLensが位置情報をロストしてオブジェクトの位置がずれてしまった場合の再設置が容易なことです.


一方でHoloLensが設置する基準座標のオブジェクトは正確な位置に配置する必要があります. またShringを行うすべてのHoloLensに対して位置合わせの操作が必要になります. 位置だけではなくオブジェクトの角度に対しても同じことが言えます. また基準位置がずれると共有しているオブジェクトすべてがずれてくるため,正確化を求めると設置の難易度は上がります.

対応策としてオブジェクトを複数点設置するようにして位置と角度の正確性を上げることもできます. 2点のオブジェクトを離れた位置に設置した場合には位置をオブジェクト間の平均から,角度を指定オブジェクトの方向に向かせることである程度改善できます.

起動位置編

HoloLensでUnityアプリを起動したとき,起動位置をHoloLensの原点(0,0,0)にするようになっています. この機能を利用することで位置合わせを行うこともできます.

  1. HoloLensを載せても移動しない台を準備する
  2. HoloLensのアプリを起動後すぐに台の上に載せる
  3. アプリ起動後に台からHololensを外して体験者にかぶせる
  4. Sharingを行う他のHoloLensも同様に同じ手順でアプリを起動する

こうすることで起動時の原点がそろい位置合わせができます. 実装が簡単でありアプリ起動時の操作さえ行えば良いので簡単です.


問題点としてはHoloLensの位置のロスト時の対応がアプリの再起動になってしまい,すぐに再設置できない場合があります. 対応策としてWorldAnchorとの併用も考えられますが,それでも位置がずれた場合には基準となる位置でアプリの再起動を行う必要があります. またHoloLensの起動基準となる台は,位置,角度を変えると位置合わせができなくなります. HoloLensの載せ方などでも位置ずれが発生する可能性があり正確性は高くないです.

画像認識編

上記の方法では環境によって位置合わせの手間と正確性が悪くなる場合がありました. そのため画像認識によるマーカーベースの位置合わせ方法があります.

HoloLensには環境認識カメラやジェスチャー認識カメラのほかにカラー映像を取得するカメラが搭載されています. このカラーカメラはUnityからでもアクセスでき画像処理を行わせることもできます. 画像処理によりマーカ認識を利用することで,固定されたマーカを見るだけで位置合わせ用のオブジェクトを設置することができます.

画像処理はHoloLensだけの機能ではなく外部のライブラリを使用します.

などがあります. 上記の画像処理ライブラリを利用することで指定されたマーカーを認識し,そこからマーカーの位置,角度を求め基準位置とすることで位置合わせが行えます.

起動位置によらず,手動で基準オブジェクトを設置する手間もかからず,形状情報の共有も行わないため位置合わせの成功率も高くなります. マーカーを再認識させれば基準位置がずれた場合でも補正が容易な面のあります.


マーカーベースによる画像認識での位置合わせの問題点としては

  • Shringを行うすべてのHOloLensがマーカーを認識する必要があること
  • 設置されたマーカー移動させられないこと

があります.マーカを見せたくない場合には基準位置設置後にはマーカーを隠してしまうのも手ですが位置ロスト後の復帰にはマーカーが必要になります.

オブジェクト位置共有方法

基準位置が設置できた後,Shringで共有するオブジェクトの位置を決めて同じ位置に見えるようにしなければなりません. そのため設置した基準位置からの相対座標を各HoloLensが共有することでオブジェクトの位置を合わせます. 自分が作っているMixedPresentationでは以下のようにして各オブジェクトの位置情報を共有しています.

  • SharingRootObjectを基準座標として配下のオブジェクトの相対座標を取得する
    • MixedPresenationManager.cs内のUpdate()でオブジェクトの位置が変更していれば基準点からの相対位置取得してデータを送信します.

MixedPresenationManager.cs

129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
void Update()
{
    if (isLoading)
    {
        for (int i = 0; i < PresentationCameras.Length; i++)
        {
            PresentationCameraControl cameracontrol = PresentationCameras[i].GetComponent<PresentationCameraControl>();
            if (Input.GetKeyUp((KeyCode)i + 49) || cameracontrol.GetTapFlag() == true)
            {
                SetPresentationCamera(i);
                jsonmessagecontrol.SendCameraMessage(i);
                Debug.Log("Camera:" + PresentationCameras[i].name);
            }
        }

        foreach (var item in MediaObject)
        {
            JsonGameObject jgo = null;
            for (int i = 0; i < MediaJsonObject.gameobject.Count; i++)
            {
                if (MediaJsonObject.gameobject[i].name == item.Key)
                {
                    jgo = MediaJsonObject.gameobject[i];
                    break;
                }
            }
            if (jgo == null)
            {
                jgo = new JsonGameObject();
                jgo.name = item.Key;
                jgo.transform.Set(item.Value.transform.localPosition, item.Value.transform.localRotation, item.Value.transform.localScale);
                MediaJsonObject.gameobject.Add(jgo);
            }
            else jgo.transform.Set(item.Value.transform.localPosition, item.Value.transform.localRotation, item.Value.transform.localScale);
                    
            MediaControl media = item.Value.GetComponent<MediaControl>();
            if (media != null)
            {
                bool flag;
                if (media.GetTapFlag(out flag))
                {
                    jsonmessagecontrol.SendPlayMessage(item.Key, flag);
                    Debug.Log("Play:" + item.Key);
                }
                if (media.GetTransform())
                {
                    jsonmessagecontrol.SendTransformMessage(jgo);
                    Debug.Log("Send:" + item.Key);
                }
            }
            else
            {
                if (item.Value.GetComponent<PresentationCameraControl>().GetTransform())
                {
                    jsonmessagecontrol.SendTransformMessage(jgo);
                    Debug.Log("Send:" + item.Key);
                }
            }
        }
        importexpot.jsonobject = MediaJsonObject;
    }
}

  • 共有するオブジェクトの名前と相対位置情報をJson形式に変換
    • JsonMessageControl.cs内で送信するJsonデータの種類とデータを作成して送信します.

JsonMessageControl.cs

63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public void SendCameraMessage(int CamNum)
{
    SendJsonMessage.type = (int)JsonMessageType.Camera;
    SendJsonMessage.CamNum = CamNum;
    udpclient.SendMessage(JsonUtility.ToJson(SendJsonMessage));
}

public void SendPlayMessage(string name,bool flag)
{
    SendJsonMessage.type = (int)JsonMessageType.Play;
    SendJsonMessage.gameobjectflag.name = name;
    SendJsonMessage.gameobjectflag.flag = flag;
    udpclient.SendMessage(JsonUtility.ToJson(SendJsonMessage));
}

public void SendTransformMessage(JsonGameObject obj)
{
    SendJsonMessage.type = (int)JsonMessageType.Transform;
    SendJsonMessage.gameobject = obj;
    udpclient.SendMessage(JsonUtility.ToJson(SendJsonMessage));
}

  • UDPのブロードキャスト送信によってローカルネットワーク内のHoloLensにデータを送る

  • 受け取ったHoloLensはデータ内のJsonデータから対象オブジェクトの相対位置を更新

MixedPresenationManager.cs

212
213
214
215
216
217
218
219
220
221
222
223
224
225
private void ReceiveTransformJsonMessage(JsonMessage jm)
{
    GameObject obj;
    if (MediaObject.TryGetValue(jm.gameobject.name, out obj))
    {
        obj.transform.localPosition = jm.gameobject.transform.position.Get();
        obj.transform.localRotation = jm.gameobject.transform.rotation.Get();
        obj.transform.localScale = jm.gameobject.transform.scale.Get();
        MediaControl mc = obj.GetComponent<MediaControl>();
        if (mc != null) mc.ResetTransform();
        else obj.GetComponent<PresentationCameraControl>().ResetTransform();
        Debug.Log("Set Receive Position");
    }
}

まとめ

  • Vuforiaによる位置合わせが楽
  • お手軽に作るなら起動位置合わせが楽
  • 複数の基準位置を設置すると離れたオブジェクトの位置合わせの補正に役立ちます.