TCP通信でHoloLensのMRC画像をPCへ送信

[HoloLens Advent Calendar 2018](https://qiita.com/advent-calendar/2018/hololens)の14日目の記事です。
HoloLensのMRC（MixedRealityCapture）画像をTCP通信を使って、PCへ送信してみました。

## 開発環境
### クライアント側
- HoloLens RS5
- Visual Studio 2017(15.9.2)
- Unity 2017.4.11f1
- HoloToolkit-Unity-2017.4.3.0.unitypackage
- HoloToolkit-Unity-Examples-2017.4.3.0.unitypackage

### サーバー側
- Windows 10 PC
- Python 3.7

## クライアント側（HoloLens）
Connectボタンをタップすることでサーバーへ接続し、タップしたときのMRC画像をサーバーへ送信します。TcpNetworkClientManager.csはアキヒロさんのを参考にSendImage関数を追加しています。コードは[こちら](https://github.com/SatoshiRobatoFujimoto/HoloLensTCPClient.git)。

<img width="960" alt="hololenstcp01.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/63863/2b18bef0-37f7-4bed-2715-3c51f96ca6aa.png">

● Client.cs

~~~csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.WSA.WebCam;
using HoloToolkit.Unity.InputModule;
using System;
using System.Linq;

public class Client : MonoBehaviour, IInputClickHandler
{

    public string IP;
    public int port;
    public Text connectButtonText;
    public TextMesh debugText;

    private PhotoCapture photoCaptureObject = null;
    private Texture2D targetTexture = null;
    private Resolution cameraResolution;

    private TcpNetworkClientManager client = null;
    
    // Use this for initialization
    void Start () {
        cameraResolution = PhotoCapture.SupportedResolutions.OrderByDescending((res) => res.width * res.height).First();
        debugText.text = cameraResolution.width.ToString() + " " + cameraResolution.height.ToString();
        targetTexture = new Texture2D(cameraResolution.width, cameraResolution.height);
        // targetTexture = new Texture2D(480, 270);
        // InputManager.Instance.PushFallbackInputHandler(gameObject);
        InputManager.Instance.AddGlobalListener(gameObject);
    }

    void OnStoppedPhotoMode(PhotoCapture.PhotoCaptureResult result)
    {
        photoCaptureObject.Dispose();
        photoCaptureObject = null;
    }

    void OnCapturedPhotoToMemory(PhotoCapture.PhotoCaptureResult result, PhotoCaptureFrame photoCaptureFrame)
    {
        photoCaptureFrame.UploadImageDataToTexture(targetTexture);
        //byte[] texByte = targetTexture.EncodeToJPG();
        //byte[] image = new byte[texByte.Length];
        //Array.Copy(texByte, image, texByte.Length);

        byte[] image = targetTexture.GetRawTextureData();
        client.SendImage(image);
        photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
    }

    public void OnInputClicked(InputClickedEventData eventData)
    {
        if (client != null)
        {
            PhotoCapture.CreateAsync(true, delegate (PhotoCapture captureObject)
            {
                photoCaptureObject = captureObject;
                CameraParameters cameraParameters = new CameraParameters();
                cameraParameters.hologramOpacity = 0.9f;
                cameraParameters.cameraResolutionWidth = cameraResolution.width;
                cameraParameters.cameraResolutionHeight = cameraResolution.height;
                cameraParameters.pixelFormat = CapturePixelFormat.BGRA32;
                photoCaptureObject.StartPhotoModeAsync(cameraParameters, delegate (PhotoCapture.PhotoCaptureResult result)
                {
                    photoCaptureObject.TakePhotoAsync(OnCapturedPhotoToMemory);
                });
            });
        }
    }

    public void ConnectButtonClicked()
    {
        if(client != null)
        {
            Debug.Log("Disconnected");
            connectButtonText.text = "Connect";
            client = null;
        }
        else
        {
            Debug.Log("Connected");
            client = new TcpNetworkClientManager(IP, port);
            connectButtonText.text = "Disconnect";
        }
    }
}

~~~

● TcpNetworkClientManager.cs

~~~csharp
using System;
#if UNITY_UWP
using System.IO;
using System.Threading.Tasks;
using Windows.Networking;
using Windows.Networking.Sockets;
#endif

public class TcpNetworkClientManager
{
#if UNITY_UWP
    private Stream stream = null;
    private StreamWriter writer = null;
#endif

    public TcpNetworkClientManager(string IP, int port)
    {
#if UNITY_UWP
        Task.Run(async () => {
            StreamSocket socket = new StreamSocket();
            await socket.ConnectAsync(new HostName(IP),port.ToString());
            stream = socket.OutputStream.AsStreamForWrite();
            writer = new StreamWriter(stream);
            StreamReader reader = new StreamReader(socket.InputStream.AsStreamForRead());
            try
            {
                string data = await reader.ReadToEndAsync();
            }
            catch (Exception) { }
            writer = null;
        });
#endif
    }

    public void SendMessage(string data)
    {
#if UNITY_UWP
        if (writer != null) Task.Run(async () =>
        {
            await writer.WriteAsync(data);
            await writer.FlushAsync();
        });
#endif
    }

    public void SendImage(byte[] image)
    {
#if UNITY_UWP
        if (stream != null) Task.Run(async () =>
        {
            await stream.WriteAsync(image, 0, image.Length);
            await stream.FlushAsync();
        });
#endif
    }
}
~~~

## サーバー側（Python）
サーバーを起動し、接続を待ちます。
接続できたら受信を待ち、画像を受信したら表示します。

● tcpserver.py

~~~python
import socket
import numpy as np
import cv2

def main():
    host = "192.168.0.31"
    port = 4000

    width = 2048
    height = 1152

    serversock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serversock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serversock.bind((host,port))
    serversock.listen(10)

    print('Waiting for connections...')
    clientsock, client_address = serversock.accept()
    print("Succeeded in Connection!")

    total = 0
    buffer_size = 4*width*height
    while True:
        data = b''
        data = clientsock.recv(buffer_size)
        print(len(data))
        if len(data) == buffer_size:
            tmp = np.frombuffer(data, np.uint8, -1)
            img = tmp.reshape(height, width, 4)
            img = cv2.resize(img, (480, 270))
            img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
            img = cv2.flip(img, 0)
            cv2.imshow("img", img)
            cv2.imwrite("out.jpg", img)
            cv2.waitKey(0)
            cv2.destroyAllWindows()
    clientsock.close()

if __name__ == '__main__':
    main()

~~~

## 実行
1．Anaconda Promptを開き、tcpserver.pyを実行しておきます。
2．HoloLensTCPClientを起動し、Connectボタンをタップし接続します。
3．タップ時に、MRC(Mixed Reality Capture)の画像を送信します。
4．サーバー側に表示されれば、完了です。
![out - rawdata.jpg](https://qiita-image-store.s3.amazonaws.com/0/63863/97adbf36-fb13-45a1-14da-3acd363e984f.jpeg)

## まとめ
- タップしたときのMRC(Mixed Reality Capture)を、TCP通信を使ってHoloLensからPCへ送信できた
- Editorで動作しないのマジ面倒くさい
- bufferデカすぎて、安定しない

## 参考文献
[1] [HoloLensでSharingする方法(TCP編)](http://akihiro-document.azurewebsites.net/post/hololens_sharingtcp/)
[2] [Python server, UWP(C#)client間でTCP通信がしたい](https://teratail.com/questions/130702)