学習済みモデルによるYOLOV4(V3)のRTPストリーミングデータ取得処理

# 概要
* 前回、運動中にRaspberryPiからMacのキャプチャ画面を保存する処理を作成しましたが、下記気づきがありました。

>[RaspberryPiとMESH(ボタンタグ+カスタムタグ)を使ったPCスクリーショット(Mac)の保存操作](https://qiita.com/7rikaz_h785/items/584739d89ac7b7095761)
> ただ、スクリーンショット操作は運動に集中しているとキーでもボタンでも押せないことや押し忘れることがあったため、自動で実行できるような仕組みがあると良いかと思いました。こちらは今後検討予定ですが、映像解析や他デバイスからのトリガーから実行できる仕組みを検討してみようと思います。

* 他デバイスからトリガーを送るのもいいですが、個人的な興味と応用の点で映像解析で検討することを考えます。
* 最終的にはZwiftなどの映像をリアルタイムに解析し、トレーニング活用ができるといいですが、まずは、解析結果を元にSSHコマンド送ることを考えます。
* 映像解析はYOLOを使用します。

# やること
1. YOLOの環境構築と動作確認
2. ストリーミングデータでの動作確認
3. 学習データの作成
4. 物体検出モデルの作成（YOLOによる学習）

## 1. YOLOの環境構築と動作確認
手持ちのJetsonNanoを使用します。

```
$ cat /etc/nv_tegra_release
# R32 (release), REVISION: 5.1, GCID: 27362550, BOARD: t210ref, EABI: aarch64, DATE: Wed May 19 18:07:59 UTC 2021
```

YOLOの環境構築はこちらの記事を参考にさせていただきました。
[Jetson NanoでYOLOv4を動かしてみました](https://denor.jp/jetson-nano%e3%81%a7yolov4%e3%82%92%e5%8b%95%e3%81%8b%e3%81%97%e3%81%a6%e3%81%bf%e3%81%be%e3%81%97%e3%81%9f)

### 動作確認
* 学習データは下記より取得しました。

```
$ wget https://pjreddie.com/media/files/yolov3-tiny.weights
$ wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v4_pre/yolov4-tiny.weights
```
#### 静止画(.jpg)の場合
* yolov3-tiny

```
$ ./darknet detector test cfg/coco.data cfg/yolov3-tiny.cfg weight/yolov3-tiny.weights data/dog.jpg
...
[yolo] params: iou loss: mse (2), iou_norm: 0.75, obj_norm: 1.00, cls_norm: 1.00, delta_norm: 1.00, scale_x_y: 1.00
Total BFLOPS 5.571 
avg_outputs = 341534 
 Allocate additional workspace_size = 52.43 MB 
Loading weights from weight/yolov3-tiny.weights...
 seen 64, trained: 32013 K-images (500 Kilo-batches_64) 
Done! Loaded 24 layers from weights-file 
 Detection layer: 16 - type = 28 
 Detection layer: 23 - type = 28 
data/dog.jpg: Predicted in 1175.981000 milli-seconds.
dog: 81%
bicycle: 38%
car: 71%
truck: 41%
truck: 62%
car: 39%
```
![predictions-dog-yolov3-tiny.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1832768/57c88391-ec5e-d209-e431-80defafea049.jpeg)

* yolov4-tiny

```
$ ./darknet detector test cfg/coco.data cfg/yolov4-tiny.cfg weight/yolov4-tiny.weights data/dog.jpg
...
[yolo] params: iou loss: ciou (4), iou_norm: 0.07, obj_norm: 1.00, cls_norm: 1.00, delta_norm: 1.00, scale_x_y: 1.05
nms_kind: greedynms (1), beta = 0.600000 
Total BFLOPS 6.910 
avg_outputs = 310203 
 Allocate additional workspace_size = 26.22 MB 
Loading weights from weight/yolov4-tiny.weights...
 seen 64, trained: 0 K-images (0 Kilo-batches_64) 
Done! Loaded 38 layers from weights-file 
 Detection layer: 30 - type = 28 
 Detection layer: 37 - type = 28 
data/dog.jpg: Predicted in 1078.379000 milli-seconds.
bicycle: 60%
dog: 84%
truck: 79%
car: 46%
```
![predictions-dog-yolov4-tiny.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1832768/f1bf6579-f083-6e99-a8be-b1a178dab035.jpeg)

yolov4の方が若干推論時間が早いです。
それぞれconfidenceに差があります。クラスの差異もあるかもしれません。

#### 動画(.mp4)の場合
* 動画ファイルはpxabayの[パグ動画](https://pixabay.com/ja/videos/%E7%8A%AC-%E3%83%91%E3%82%B0-%E5%8B%95%E7%89%A9-%E3%83%9A%E3%83%83%E3%83%88-45588/)を960X540のサイズでDLして確認しました。
* 結果は表示される動画で確認しましたが、動画はファイルサイズが大きいためgifで保存しています。また、コマンド実行時は後処理としてバウンディングボックスで検出結果を表示しますが、動作を考慮すると-dont_showオプションをつけた方がいいかもしれません。
* 出力ファイルはパスが違いますが、Githubに追加しました。
[frdg11ykCgh/get-yolo-recognize-result/result/Dog_45588/](https://github.com/frdg11ykCgh/get-yolo-recognize-result.git)

* yolov3-tiny

```
$ ./darknet detector demo cfg/coco.data cfg/yolov3-tiny.cfg weight/yolov3-tiny.weights data/Dog-45588.mp4 > Dogmp4_tinyv3.txt
```
* 出力動画(.gifに変換)
![Dogmp4_tinyv3_f72_500msec.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1832768/966b6f59-8dab-723e-f620-1847f81f9435.gif)
 
* yolov4-tiny 

```
$ ./darknet detector demo cfg/coco.data cfg/yolov4-tiny.cfg weight/yolov4-tiny.weights data/Dog-45588.mp4 > Dogmp4_tinyv4.txt
```

* 出力動画(.gifに変換)  
![Dogmp4_tinyv4_f72_500msec.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1832768/45717b5c-8855-9b45-9ab9-a85351faab1f.gif)

gifを見ていただくとわかりますが、yolov3-tinyは検出ができていません。
confidenceも0%でした。dog.jpgで犬を検出しているのでパグも犬として検出するかと思いましたが、動画では検出ができないようです。これは学習済みモデルの問題かどうかは判断できていませんが、静止画では５０％で検出ができていました(Peasonも40%で検出していますが）。

```
$ ./darknet detector test cfg/coco.data cfg/yolov3-tiny.cfg weight/yolov3-tiny.weights data/test_pug.jpg > test_pug.txt
...
net.optimized_memory = 0 
mini_batch = 1, batch = 1, time_steps = 1, train = 0 
Create CUDA-stream - 0 
 Create cudnn-handle 0 

 seen 64, trained: 32013 K-images (500 Kilo-batches_64) 
 Detection layer: 16 - type = 28 
 Detection layer: 23 - type = 28 
data/test_pug.jpg: Predicted in 1100.094000 milli-seconds.
person: 40%
dog: 50%
```

## 2. ストリーミングデータでの動作確認
* 動画による確認ができたため本題のストリーミングデータによる確認をします。
* 細かくは見ていませんが、srcフォルダを確認すると、darknetコマンドを引数に`demo`指定で実行するとdarknet.cのmain()関数→detector.cのrun_detector()関数→demo.cのdemo()関数の順にコールします。
* demo()関数ではfilename参照し、image_opencv.cppのget_capture_video_stream(filename)を実行します。
* filenameはコマンドライン引数で指定するストリームURLに相当します。つまり、OpenCVの仕様を満たせばストリーミングデータの受信は可能ということになります。

```darknet.c
int main(int argc, char **argv)
{
…
} else if (0 == strcmp(argv[1], "detector")){
        run_detector(argc, argv);
…
```

```detecter.c
void run_detector(int argc, char **argv)
{
…
char *filename = (argc > 6) ? argv[6] : 0; //コマンドライン引数で指定するストリームURL
…
   else if (0 == strcmp(argv[2], "demo")) {
        list *options = read_data_cfg(datacfg);
        int classes = option_find_int(options, "classes", 20);
        char *name_list = option_find_str(options, "names", "data/names.list");
        char **names = get_labels(name_list);
        if (filename)
            if (strlen(filename) > 0)
                if (filename[strlen(filename) - 1] == 0x0d) filename[strlen(filename) - 1] = 0;
        demo(cfg, weights, thresh, hier_thresh, cam_index, filename, names, classes, avgframes, frame_skip, prefix, out_filename,
            mjpeg_port, dontdraw_bbox, json_port, dont_show, ext_output, letter_box, time_limit_sec, http_post_host, benchmark, benchmark_layers);

        free_list_contents_kvp(options);
        free_list(options);
    }
…
```

```demo.c
void demo(char *cfgfile, char *weightfile, float thresh, float hier_thresh, int cam_index, const char *filename, char **names, int classes, int avgframes,
    int frame_skip, char *prefix, char *out_filename, int mjpeg_port, int dontdraw_bbox, int json_port, int dont_show, int ext_output, int letter_box_in, int time_limit_sec, char *http_post_host,
    int benchmark, int benchmark_layers)
{
...
    if(filename){
        printf("video file: %s\n", filename);
        cap = get_capture_video_stream(filename);
        demo_skip_frame = is_live_stream(filename);
    }else{
        printf("Webcam index: %d\n", cam_index);
        cap = get_capture_webcam(cam_index);
        demo_skip_frame = true;
    }
...
```

* 通信はプライベートスコープのマルチキャストアドレス指定で設定します。  
  設定はこちらの記事を参考にさせていただきました。  
[OBSとVLCでLAN内ストリーミング](https://tksoft.work/archives/1803)
* ストリーミングデータはMP4形式と同様にpxabayの[パグ動画](https://pixabay.com/ja/videos/%E7%8A%AC-%E3%83%91%E3%82%B0-%E5%8B%95%E7%89%A9-%E3%83%9A%E3%83%83%E3%83%88-45588/)を再生し、RTP指定でdarknetに入力します。
* 今回は検出結果を元にSSHコマンドを実行するため、引数に`-json_port`を指定して、node.js側で受信処理します。(コードはGitHubにもコミットしました。)

```yolo_get_outputjson.js
const request = require('request')
const JSONStream = require('JSONStream')

// YOLO:Real-Time Objection Dection output json
var YOLO_HOST = // ./darknet ip adress
var YOLO_PORT = 5000 // ./darknet json_port

function yolo_json_output(){
	console.log('yolo : ' + YOLO_HOST + " : " + YOLO_PORT)
	const stream = request('http://' + YOLO_HOST + ':' + YOLO_PORT).pipe(JSONStream.parse('$*'))
	stream.on('data', (data) => {
		var fid  = data.value.frame_id
		var objs = data.value.objects
		objs.forEach(v => {
			if (!(v.name === '')) {
				console.log('------------------------------');
				console.log('frame_id:',fid)
				console.log('name:',v.name)
				console.log('confidence:',v.confidence)
			}
		}); // end of objs.forEach
	}); // end of stream.on
}

yolo_json_output();
```

### 動作確認
* 出力ファイルはパスが違いますが、Githubに追加しました。
* yolov3-tiny

```
$ ./darknet detector demo cfg/coco.data cfg/yolov3-tiny.cfg weight/yolov3-tiny.weights rtp://@239.1.1.5:8004 -json_port 5000 > OBS_Dog_tinyv3.txt
```
* 出力動画(.gifに変換)
![OBS_Dog_tinyv3_f72_500msec.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1832768/9033f407-be4e-ffeb-1064-c6ab4ebeea20.gif)



* yolov4-tiny

```
$ ./darknet detector demo cfg/coco.data cfg/yolov4-tiny.cfg weight/yolov4-tiny.weights rtp://@239.1.1.5:8004 -json_port 5000 > OBS_Dog_tinyv4.txt
```

* 出力動画(.gifに変換)
![OBS_Dog_tinyv4_f72_500msec.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1832768/7dcc5178-e30f-75c0-1f15-a9257bf72307.gif)

* gifファイルのため、画像は小さいですが、ストリーミングデータでも検出できることが確認できます。yolov3-tinyでも検出できています。
* 出力結果を確認するとcat,dogと検出していました。学習データとしては検出したいクラスに特化したデータの方がいいのでしょうか。
* 個人的には形や動き方などの情報を追加するとより精度が上がるのかも知れないと思いました。結果の出力まで時間がかかるようでは意味がありませんが。
* システムとして考えると学習データは単一ではなく、目的に応じて複数のデータを活用する仕組みが理想かとも思いました。

#### 検出結果からコマンド実行処理
* 例として、[RaspberryPiとMESH(ボタンタグ+カスタムタグ)を使ったPCスクリーショット(Mac)の保存操作](https://qiita.com/7rikaz_h785/items/584739d89ac7b7095761)で作成したコードを使用してAppleScriptを実行できることを確認しました。
* SSHコマンドの実行間隔が短いと認証エラーになりますが、トリガーとして使用できることは確認できました。

```.js
const request = require('request')
const JSONStream = require('JSONStream')
const {exec} = require('child_process')

// YOLO:Real-Time Objection Dection output json
var YOLO_HOST = // ./darknet ip adress
var YOLO_PORT = // ./darknet json_port

const sshcmd = "ssh [接続するsshのエイリアス名]"
//ファイル形式
const fileformat = "defaults write com.apple.screencapture type [ファイル形式]"
//画像ファイルの保存先パス
const savefilepath ="defaults write com.apple.screencapture location [画像ファイルの保存先パス] "
//AppleScriptのパス
const applescriptpath = "osascript [AppleScriptのパス] "
//AppleScript実行（ファイル形式と保存先指定）
//const cmd = sshcmd + fileformat + "&& " + sshcmd + savefilepath + "&& " + sshcmd + applescriptpath
//AppleScript実行
const cmd = sshcmd + applescriptpath

function yolo_json_output(){
	console.log('yolo : ' + YOLO_HOST + " : " + YOLO_PORT)
	const stream = request('http://' + YOLO_HOST + ':' + YOLO_PORT).pipe(JSONStream.parse('$*'))
	stream.on('data', (data) => {
		var fid  = data.value.frame_id
		var objs = data.value.objects
		objs.forEach(v => {
			if ((v.name === 'dog')&&(v.confidence > 0.8)) {
				console.log('------------------------------');
				console.log('frame_id:',fid)
				console.log('name:',v.name)
				console.log('confidence:',v.confidence)

				exec(cmd, (error, stdout, stderr) =>{
					if (error) {
						console.error(`exec error: ${error}`);
            					return;
				        }
				}) // end of exec
			}
		}); // end of objs.forEach
	}); // end of stream.on
}

yolo_json_output();
```

## 今後について
やることの内、1,2まで確認できました。YOLOの環境構築から実行までできましたが、モデルの差異やモデルの作成方法に関する知識の必要性を感じたため、今後は知識を深めつつ、学習データの作成から物体検出モデルの作成を行う予定です。