THETA x Hololensで視覚能力拡張

#はじめに 
はじめまして、リコーの[Yuuki_S](https://qiita.com/Yuuki_S)です。
弊社では[RICOH THETA](https://theta360.com/ja/)という全周囲360度撮れるカメラを出しています。
RICOH THETA VやRICOH THETA Z1は、OSにAndroidを採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます
（詳細は本記事の末尾を参照）。


さて、THETAは周囲360°の映像が取得可能なカメラです。
360°、これは人間の視野(およそ200°)に比べて遥かに広いです。
しかし、自然界にはTHETAに近い視野を持つ存在がいます。
それは馬に代表される草食動物。
彼らの目は頭の側面に位置し、およそ330°の範囲を捉えます。
広い範囲を捉えることで、外敵を素早く察知する事が出来るのです。
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/4d1957b8-c84c-8721-b7f6-bdb24eb1cd1c.png)

馬とは違い、正面にしか目を持たない人間ですが、自らのテクノロジーによって能力を拡張する事が可能です。
そう、THETAがあれば....
![図1修正後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/4b960184-80cc-35a0-b397-7949840821fa.png)



...見た目で出オチ感がありますが、今回はTHETAプラグインを用いHololensで側面を見れる視覚能力拡張装置を作ってみました。まずは、動作の様子を動画でご覧ください。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">広い視野で差をつけろ！指揮官機用Hololensで索敵能力UP！ <a href="https://twitter.com/hashtag/THETA?src=hash&amp;ref_src=twsrc%5Etfw">#THETA</a> <a href="https://twitter.com/hashtag/thetaplugin?src=hash&amp;ref_src=twsrc%5Etfw">#thetaplugin</a> <a href="https://twitter.com/hashtag/Hololens?src=hash&amp;ref_src=twsrc%5Etfw">#Hololens</a><br><br>Qiita記事はこちら↓<a href="https://t.co/peFoJRti0b">https://t.co/peFoJRti0b</a> <a href="https://t.co/KGkJswfabO">pic.twitter.com/KGkJswfabO</a></p>&mdash; THETAプラグイン開発者コミュニティ (@thetaplugin) <a href="https://twitter.com/thetaplugin/status/1204574163601481728?ref_src=twsrc%5Etfw">2019年12月11日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


#何をしているのか 
やっていることはシンプルで、THETAで取得した画像をSocket通信でHololensに送り処理後に表示しているだけです。
![図3_Fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/8d6a7ec4-1b50-3fca-dea9-b9a1f7f99672.png)

THETA側は全天球画像の取得とその送信をプラグインで実装。
Hololens側はそれを受信、左右側面に該当する領域を切り出し描画しています。
以下でそれぞれに関して説明します。

#THETA側プラグイン 
THETAからの画像取得までは以下の記事を参考にさせて頂きました。
[THETAの中でOpenCVを動かす【プレビューフレーム取得編】](https://qiita.com/roohii_3/items/f96918cdb52fd84e1cea)
上記の記事の環境からさらに送信部分を追加した形です。

今回、画像の送信にはTCP/IPによるSocket通信を用います。
まず、"MainActivity.java"のonCreateにて送信するビットマップを作成します。
そして、OpenCVにてカメラフレームを取得している箇所で、取得フレームをMat形式からビットマップ形式に変換します。

```java
Bitmap SendBmp;
 ...
  protected void onCreate(Bundle savedInstanceState)
    {
     ...
        SendBmp = Bitmap.createBitmap(640, 320, Bitmap.Config.ARGB_8888);
     ...
    }
    
 public Mat onCameraFrame(CvCameraViewFrame inputFrame)
    {
        Mat SendMat = inputFrame.rgba();
        Utils.matToBitmap(SendMat,SendBmp);
        connect();//Socket通信
        
        mOutputFrame = SendMat;//プレビュー用に引き渡し
        return mOutputFrame;
    }
```

あとは変換後の画像送信ですが、connect()という関数が実際の送信処理をおこないます。
AndroidではSocket通信は別スレッドで処理する必要があるので、AsyncTaskで実装します。
Socketの設定をして、byte配列にして送るだけですが、ビットマップそのままだと容量が大きいためJPEGに圧縮しています。

```java:Socket通信部分
public void connect()
    {
        new AsyncTask<Void, Void, String>()
        {
            @Override
            protected String doInBackground(Void... voids)
            {
                try
                {
                    socket = new Socket("192.168.100.51",12345);
                    OutputStream Out = socket.getOutputStream();
                    ByteArrayOutputStream Bos = new ByteArrayOutputStream();
                    SendBmp.compress(CompressFormat.JPEG, 20,Bos);
                    byte[] jpgarr = Bos.toByteArray();
                    Log.i(TAG, "Send."+jpgarr.length);
                    Out.write(jpgarr);
                    Out.flush();
                    socket.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
                return null;
            }
        }.execute();
    }
```

最後に、AndroidManifest.xmlに通信のパーミッションを追加します。

```java:AndroidManifest.xml
  ...
  <uses-permission android:name="android.permission.INTERNET" />
  ...
```
これでプラグインを起動すれば指定したIPアドレスに画像を送信できるようになりました。

#Hololens側のアプリケーション 
Hololensのアプリケーション開発は、Unityを用いておこないます。
細かな環境構築に関しては割愛しますが、各種設定はMRTK(Microsoft Mixed Reality Toolkit)をインポートして使うと便利です。通信周りに関しては、@akihiro01051さんの[HoloLensModule](https://github.com/akihiro0105/HoloLensModule)を利用させて頂きました。

処理の構成は、画像の受信→球面への展開→カメラによる側面領域の画像取得(RenderTexture)→取得画像の表示 の4段階です。

まず、画像の受信部分は、以下の通り実装しています。
(Hololens実機上で動作するUWP部分のみ抜粋して記載しています。)

```cs
...
#if WINDOWS_UWP
using System.Threading.Tasks;
using Windows.Networking.Sockets;
using System.IO;
using System.Diagnostics;
#else
using System.Net.Sockets;
#endif

namespace HoloLensModule.Network
{
    public class TCPServer
    {
        public delegate void ListenerMessageEventHandler(string ms);
        public event ListenerMessageEventHandler ListenerMessageEvent;
        public delegate void ListenerByteEventHandler(byte[] data);
        public event ListenerByteEventHandler ListenerByteEvent;
        byte[] receivedData;
#if WINDOWS_UWP
        private StreamSocketListener socketlistener = null;
#else
        private TcpListener tcpListener = null;
#endif
        private bool isActiveThread = true;
       
        public TCPServer(int port)
        {
            ConnectServer(port);
        }
        
        public void ConnectServer(int port)
        {
#if WINDOWS_UWP
            Task.Run(async () =>
            {
                socketlistener = new StreamSocketListener();
                socketlistener.ConnectionReceived += ConnectionReceived;
                await socketlistener.BindServiceNameAsync(port.ToString());
            });
#else
           ...
#endif
        }

        public void DisConnectClient()
        {
#if WINDOWS_UWP
            socketlistener.Dispose();
#else
          ...
#endif
            isActiveThread = false;
        }


#if WINDOWS_UWP
        private async void ConnectionReceived(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args)
        {          
            var reader = new BinaryReader(args.Socket.InputStream.AsStreamForRead());
            var bytes = new byte[65536];
            while (isActiveThread)
            {     
                await Task.Delay(10);
                try
                {                  
                    var num = reader.Read(bytes, 0, bytes.Length);

                    if (num > 0)
                    {
                        receivedData = new byte[num];
                        Array.Copy(bytes, 0, receivedData, 0, num);                      
                        if (ListenerMessageEvent != null) ListenerMessageEvent(Encoding.UTF8.GetString(receivedData));
                        if (ListenerByteEvent != null) ListenerByteEvent(receivedData);
                        await reader.BaseStream.FlushAsync();
                    }                     
                }
                catch (Exception e){}
            }
        }
#else
      ...
#endif
    }
}
```

そして、上記で取得した画像をテクスチャとして反映するスクリプトを作ります。

```cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using HoloLensModule.Network;
using HoloLensModule.Environment;
using System;
using System.Threading;
using System.IO;
public class TextureFromTCP : MonoBehaviour
{
    public GameObject TargetPlane;
    private TCPServer tcpServer;
    private Texture2D texture;
    private Renderer rend;
    private SynchronizationContext currentcontext;
    byte[] GettedTex;

    // Start is called before the first frame update
    void Start()
    {
        tcpServer = new TCPServer(12345);
        tcpServer.ListenerByteEvent += ListenerByteEvent;
        rend = TargetPlane.GetComponent<Renderer>();
        texture = new Texture2D(640, 320);
        currentcontext = SynchronizationContext.Current;
        GettedTex = new byte[640 * 320];
    }

    private void ListenerByteEvent(byte[] data)
    {             
        GettedTex = new byte[data.Length];
        GettedTex = data;     
    }

    // Update is called once per frame
    void Update()
    {    
      texture.LoadImage(GettedTex);
      rend.material.mainTexture = texture;
    }

    private void OnDestroy()
    {
        tcpServer.DisConnectClient();
    }
}
```
このスクリプトを法線内向きの球体に貼り付ければ、受信した全天球が展開できます。
法線内向きの球体は以下のページにある伊藤周(@warapuri)さんのSphere100.fbxを使わせて頂きました。
[UnityとOculusで360度パノラマ全天周動画を見る方法【無料編】](https://warapuri.com/post/131599525953/unity%E3%81%A8oculus%E3%81%A7360%E5%BA%A6%E3%83%91%E3%83%8E%E3%83%A9%E3%83%9E%E5%85%A8%E5%A4%A9%E5%91%A8%E5%8B%95%E7%94%BB%E3%82%92%E8%A6%8B%E3%82%8B%E6%96%B9%E6%B3%95%E7%84%A1%E6%96%99%E7%B7%A8)
![球面への展開.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/9b4e20f4-5c9b-e88f-769b-071f9eb9274e.png)

これで9割ほど準備ができました。
あとは、球体の内側に右側と左側それぞれの映像を取得するカメラを配置します。

このカメラの出力は直接Hololensに表示しないため、RenderTextureを描画先に設定します。
そして、このテクスチャを描画する平面を左右用に計2枚作成し、RenderTextureを反映し、完成です。

![Hololens描画平面.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/226c6148-4c04-7f83-faaa-a41599816a21.png)

ただ、このままでは四角形すぎて目立つので、以下のシェーダーで周辺をぼかすようにしています。
https://github.com/fand/UnityMaskShader
![シェダ-で見た目改善.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/cba238a0-2ef8-12b0-2a6a-8f7760f9c6e1.png)
※プレビューでは縁が黒になっていますが、Hololens上では黒は透過するため、エッジがボケた見た目になります。

#実行 
作成したTHETAのプラグインをVysor経由で起動します。
(事前にTHETAの無線LANをONにし、Hololensと同じネットワークに接続しておきます)
プレビューが表示されれば、送信処理が実行されています。
ホントは送信状況を画面に表示すると良いですが、今回は省略しています。
![キャプチャ3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/a90ae666-fb01-d140-caa4-6793a9e4fa74.png)

なお、THETAとパソコンをUSBケーブルで繋いでいる状態ではWi-Fi接続が不安定になる場合があります。
その様な場合は、以下のページに記載のABDコマンドを実行してください。
[THETAプラグイン開発におけるVysorの使い方 9.Vysorを使ってWiFiとテストアプリを設定する](https://qiita.com/KA-2/items/9dce1a165fcf0203b1cb#9-vysor%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6wifi%E3%81%A8%E3%83%86%E3%82%B9%E3%83%88%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)

***
次にHololens側でアプリケーションを起動させます。
問題なければ、THETAから送られた左右の視点画像が表示されます。

![20191206_120357_HoloLens.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/70d7dd06-bcd1-83e4-2422-cc30028546b1.jpeg)
スクリーンショットでは黒い縁が見えてますが、実際には以下のようにボヤケています。
![キャプチャ5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/410f573c-5241-81fd-f5b6-b1b6c0068c36.png)

自分の視野を超えた範囲に最初は戸惑いますが、慣れるとなかなか良いSF感。

#THETAとHololensのドッキング 
最後に物理的にHololensとTHETAを繋げます。
今回、Hololensのマウント部分にはHoloLab Incさんが上げているLeap Motionとの接続マウントを流用させて頂きました。
https://github.com/HoloLabInc/3dPrintableModels
上記にTHETAの三脚穴に挿す1/4インチネジの部分を追加設計して、取り付けています。

![正面.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/83bb8039-f6df-6991-c305-4be5d1949c79.jpeg)
![斜め.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/9845282f-25fc-769e-fb6d-40e3c82ff63f.jpeg)
(通常のHololensより指揮官機感が出ます。)

#まとめ 
視覚能力拡張というアイディアでTHETAの画像をHololensに送って利用する方法を記載しました。
THETAの360°すべて撮影できるという特徴を上手く利用することで、1個の装置で様々な利用法が生まれます。
今回はリアルタイムに電送して表示することを試しましたが、次は撮影画像に加工するようなアプリケーションを作ろうと思います。
出来上がったらまた投稿しますので、ご期待ください。


#RICOH THETAプラグインパートナープログラムについて 
THETAプラグインをご存じない方は[こちら](https://api.ricoh/products/theta-plugin/)をご覧ください。
パートナープログラムへの登録方法は[こちら](https://qiita.com/mShiiina/items/55d98f366e650ca42251)にもまとめてあります。
興味を持たれた方は[Twitter](https://twitter.com/thetaplugin)のフォローと[THETAプラグイン開発コミュニティ(Slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU)への参加もよろしくおねがいします｡