Deep写輪眼とUEをTCPで連携させる

# 概要
- Deep写輪眼とUEを連携させます。
- Deep写輪眼はPython環境でWebカメラから手で組んだ印を検出します。
    - https://github.com/Kazuhito00/NARUTO-HandSignDetection
    - https://qiita.com/Kazuhito/items/ff3b04437617427f55a1
- Deep写輪眼はUE側とは独立して動作し毎フレームの検出結果をTCPでUEに送信します。
- UEはDeep写輪眼の検出結果をTickで取得します。
- UEでTCPを使用するためにObject Deliverを使用します。
    - https://www.unrealengine.com/marketplace/ja/product/objectdeliverer

# 注意点
- Deep写輪眼の環境構築方法の詳細は公式をご確認ください。
- Deep写輪眼とUEはそれぞれ独立して動作します。Deep写輪眼はコンソールから実行します。一つのexeにまとめることや、Steam等で配布することは考慮していません。有識者～～～～


# 使用例
こういうことができます。
https://www.youtube.com/watch?v=sAgMQHW2FWQ

# Deep写輪眼の検出結果を送信する
## Deep写輪眼を動かす
Deep写輪眼をローカル環境で動かせる状態にしてください。リポジトリ内の「simple_demo.py」が実行できれば問題ないです。
- https://github.com/Kazuhito00/NARUTO-HandSignDetection
- https://qiita.com/Kazuhito/items/ff3b04437617427f55a1


### 環境構築方法
※ 2021年頃に書いたので内容が最新でない可能性があります。バージョン等は適宜読み替えてください。
わかってる人は読み飛ばしてください。公式の方が詳しいと思います。
手元のPCのGPU等によって若干変わります。一例として読んでください。
ChatGPT4が結構丁寧に教えてくれます。


#### Cuda cuDNNのインストール
CUDA Toolkit 11.0をダウンロードインストールします
https://developer.nvidia.com/cuda-11.0-download-archive

cuDNNをインストールします
https://developer.nvidia.com/rdp/cudnn-archive

Download cuDNN v8.0.5 (November 9th, 2020), for CUDA 11.0をクリック
cuDNN Library for Windows (x86)　をダウンロードします。
フォルダを解凍後、ダウンロードしたデータのcuda配下のすべてのフォルダを以下の位置にコピーします。

C:\ProgramFile\NVIDIA GPU Computing Toolkit\CUDA\v11.0\

#### Anacondaの設定

以下のページからAnacondaをdowloadしてinstallしてください
https://www.anaconda.com/products/individual

64-Bit Graphical Installer (457 MB)をinstallします
Install中に
「add Anaconda to the system Pass environment variable」
のチェックボックスにチェックを入れます。


#### 必要なパッケージのインストール
「コマンドプロンプト」を起動し以下のコマンドを順に実行します。


以下のコマンドを順に実行し必要なパッケージをインストールします。わかる人は仮想環境作ったらいいと思います。
```
python -m pip install --upgrade --user pip
conda install -c conda-forge opencv       
conda install tensorflow-gpu　
pip install numpy
pip install sockets
```

以上で環境構築が完了です。
#### 動作確認
simple_demo.py　をダブルクリックして起動します。
起動しないときは以下のコマンドを実行してください。
```
python simple_demo.py
```
ウィンドウが開きウェブカメラの映像が出たらDeep写輪眼の起動準備は終了です。

## TCPで送信できるようにする。
「simple_demo.py」を検出結果を送信できるように書き換えます。
TCPに関する説明はこのあたりを参照したらいいと思います。
https://qiita.com/t_katsumura/items/a83431671a41d9b6358f

最終的に以下の部分で検出結果を送信しています。
```
conn.send(bytes(labels[class_id][0], 'utf-8'))#ラベルの送信
```

書き換えた後のコードは次の通りになります。並立でUEを動かす都合上パフォーマンスを下げてます。configの値は公式のREADMEを参照してください。

30010のポートを使用することを想定しています。

simple_demo_unreal.pyを実行すると「WaitForStart」のログが出て停止します。
後述の手順を行い受信できる状態にしたUE側を起動すると、諸々の処理が実行され接続後に起動します。
公式の「simple_demo.py」が動作できる環境なら動作します。

```simple_demo_unreal.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import argparse
import csv
import time
import copy

import cv2 as cv
import numpy as np
import tensorflow as tf

# socket サーバを作成
import socket


def get_args():
    parser = argparse.ArgumentParser()

    parser.add_argument("--device", type=int, default=0)
    parser.add_argument("--file", type=str, default=None)
    parser.add_argument("--fps", type=int, default=10)
    parser.add_argument("--width", help='cap width', type=int, default=960)
    parser.add_argument("--height", help='cap height', type=int, default=540)

    parser.add_argument("--model", default='model/EfficientDetD0/saved_model')
    parser.add_argument("--score_th", type=float, default=0.75)
    parser.add_argument("--skip_frame", type=int, default=0)

    args = parser.parse_args()

    return args


def run_inference_single_image(image, inference_func):
    tensor = tf.convert_to_tensor(image)
    output = inference_func(tensor)

    output['num_detections'] = int(output['num_detections'][0])
    output['detection_classes'] = output['detection_classes'][0].numpy()
    output['detection_boxes'] = output['detection_boxes'][0].numpy()
    output['detection_scores'] = output['detection_scores'][0].numpy()
    return output


def main():
    # 引数解析 #################################################################
    args = get_args()
    cap_device = args.device
    cap_width = args.width
    cap_height = args.height
    fps = args.fps
    skip_frame = args.skip_frame

    model_path = args.model
    score_th = args.score_th

#サーバーに接続    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # IPアドレスとポートを指定
    s.bind(('127.0.0.1', 30010))
    # 1 接続
    s.listen(1)
    # connection するまで待つ
    print("WaitStartGame")
    conn, addr = s.accept()

    if args.file is not None:
        cap_device = args.file

    frame_count = 0

    # カメラ準備 ###############################################################
    cap = cv.VideoCapture(cap_device)
    cap.set(cv.CAP_PROP_FRAME_WIDTH, cap_width)
    cap.set(cv.CAP_PROP_FRAME_HEIGHT, cap_height)

    # モデルロード #############################################################
    DEFAULT_FUNCTION_KEY = 'serving_default'
    loaded_model = tf.saved_model.load(model_path)
    inference_func = loaded_model.signatures[DEFAULT_FUNCTION_KEY]

    # ラベル読み込み ###########################################################
    with open('setting/labels.csv', encoding='utf8') as f:
        labels = csv.reader(f)
        labels = [row for row in labels]
    print("InitiarizeDetection")

    while True:
        start_time = time.time()

        # カメラキャプチャ #####################################################
        ret, frame = cap.read()
        if not ret:
            continue
        frame_width, frame_height = frame.shape[1], frame.shape[0]
        debug_image = copy.deepcopy(frame)

        frame_count += 1
        if (frame_count % (skip_frame + 1)) != 0:
            continue

        # 検出実施 #############################################################
        frame = frame[:, :, [2, 1, 0]]  # BGR2RGB
        image_np_expanded = np.expand_dims(frame, axis=0)

        output = run_inference_single_image(image_np_expanded, inference_func)

        num_detections = output['num_detections']
        for i in range(num_detections):
            score = output['detection_scores'][i]
            bbox = output['detection_boxes'][i]
            class_id = output['detection_classes'][i].astype(np.int)

            if score < score_th:
                continue

            # 検出結果可視化 ###################################################
            x1, y1 = int(bbox[1] * frame_width), int(bbox[0] * frame_height)
            x2, y2 = int(bbox[3] * frame_width), int(bbox[2] * frame_height)

            cv.putText(
                debug_image, 'ID:' + str(class_id) + ' ' +
                labels[class_id][0] + ' ' + '{:.3f}'.format(score),
                (x1, y1 - 15), cv.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2,
                cv.LINE_AA)
            cv.rectangle(debug_image, (x1, y1), (x2, y2), (0, 255, 0), 2)

            print('StartConnect')
            conn.send(bytes(labels[class_id][0], 'utf-8'))#ラベルの送信

        # キー処理(ESC：終了) #################################################
        key = cv.waitKey(1)
        if key == 27:  # ESC
            break

        # FPS調整 #############################################################
        elapsed_time = time.time() - start_time
        sleep_time = max(0, ((1.0 / fps) - elapsed_time))
        time.sleep(sleep_time)

        cv.putText(
            debug_image,
            "Elapsed Time:" + '{:.1f}'.format(elapsed_time * 1000) + "ms",
            (10, 30), cv.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2, cv.LINE_AA)

        # 画面反映 #############################################################
        cv.imshow('NARUTO HandSignDetection Simple Demo', debug_image)
        # cv.moveWindow('NARUTO HandSignDetection Simple Demo', 100, 100)

    cap.release()
    cv.destroyAllWindows()


if __name__ == '__main__':
    main()
```


# UE側で検出結果を受け取る
Deep写輪眼から送信された検出結果をUE側で受け取るようプロジェクト側を修正します。

## ObjectDeliverの追加
Object Deliverプラグインを追加してください。
- Object Deliver
    - https://www.unrealengine.com/marketplace/ja/product/objectdeliverer
- README
    - https://github.com/ayumax/ObjectDeliverer

## 受信用Actorの設定
TcpSocketConnection Classを作成します。
![TcpSocketConnection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638047/07b51fc9-f3d1-cd16-b553-0a3d2dd39b74.png)

以下のような処理を追加します。画像はUE4ですがUE5でも同じだと思います。

![BP_Tcp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638047/b3a2159a-3182-4dfd-3a7e-2c2ed7d118f4.png)

これをレベル上に置いておくとTickでDeep写輪眼の値を取得します。
ここではGI_DetectedLabelに検出結果が入ります。
検出結果がString型で「Ne(Rat)」のように入ってきます。
今回はGameInstanceに送ってあちこちで使っていますが、この辺はよしなにやってください。

![BP_DetectConverter-Detect Converter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638047/3b2b5eec-5fa9-07c0-a755-a211f4153a64.png)

# 使い方
環境構築が済んでいる前提で説明をします。
1. simple_demo_unreal.pyを実行します。
1. 「WaitForStart」のログが出ていることを確認します
1. UEプロジェクトをPlayします。Play in EditorではなくスタンドアローンでPlayしないと通信されない時があります。
1. Deep写輪眼が起動します。しばらく時間がかかります。ログが「WaitForStart」から流れ始めれば正常に動作しています。
1. Webカメラが起動します。
1. 検出結果がUE側に送信されていることを確認します


# 応用例
Deep写輪眼を別のリポジトリに置き換えても同じ要領でUEと連携できます。
これはMediaPipeを使用して作った「ういびーむDetector」です。
絵は10秒で描きました。たすけてしぐれうい。

https://www.youtube.com/watch?v=IOar5YcfoRE

# 知りたいこと
機械学習関係の処理をUE内に埋め込むのどうやるんですか。
ゲームやろうとして「Anaconda入れてYAML叩いて環境構築してね」はハードル高い気がしてます。
Deep写輪眼まで含めて1つのexeにまとめてワンチャンSteamに公開するところまでいけないですか。onnxに対応したらしいですが、ほとんど情報が見つからないです。有識者～～～
https://historia.co.jp/archives/26238/