PythonのWebRTC実装aiortcと、物体検出のYOLO v3の連携を試みる

# はじめに

* [aiortc](https://github.com/jlaine/aiortc) は、Pythonで実装されたWebRTCのライブラリです
    * 参考: [PythonのWebRTC実装を、Dockerで動かして見た](https://qiita.com/massie_g/items/a9539c0b0e5d83645bc5)
* YOLO v3はC言語で作られた機械学習のフレームワーク[darknet](https://pjreddie.com/darknet/)上で動く、物体検出のネットワークです。
    * darknet の [GitHub](https://github.com/pjreddie/darknet)
    * 参考： [Docker上でPython3からYOLO v3 を動かしてみた](https://qiita.com/massie_g/items/a2bcfac4fed66b1b0717)


今回はこの2つを連携させて、Docker上で動かすことを試みました。先に結果を示します。

- 連携自体はできて、物体検出も可能
- CPUで動かしているため、1フレームあたり1秒以上かかる。動画をリアルタイムで処理するのは無理
    - 映像がどんどん遅れていく → [スレッドを利用して映像を止めないようにしました](https://qiita.com/massie_g/items/20aa3a9d0e04db7e7fab)
- GPUで動かせば速くなるはずだが（未検証）、秒間30フレームに追従するのは難しそう

今回作ったサンプルのコードはこちらです。

 - https://github.com/mganeko/aiortc_yolov3


# 環境

- ホストOS: Mac OS X 10.12.6 (Sierra)
- Docker: Docker for Mac Version 17.06.0-ce-mac18 (18433)
- コンテナのベース: Ubuntu 18.04 LTS

# Dockerfile

使ったDockerfileはこちらです。

- python3
- libopus-dev, libvpx-dev
- libffi-dev, libssl-dev
- libopencv-dev (2018.07.03 追加)
- aiortc
- darknet
- YOLO v3 モデル
- 今回のサンプル ([GitHub](https://github.com/mganeko/aiortc_yolov3))

等をインストールしています。

```docker
# Ubuntu 18.04 and aiortc
#  aiortc: https://github.com/jlaine/aiortc

FROM ubuntu:18.04
MAINTAINER mganeko

#  
# -- if you are using docker behind proxy, please set ENV --
#

#ENV http_proxy "http://proxy.yourdomain.com:8080/"
#ENV https_proxy "http://proxy.yourdomain.com:8080/"

ENV DEBIAN_FRONTEND nonineractive

#
# -- build tools --
#
RUN apt update && apt upgrade -y

RUN apt install python3 -y
RUN apt install python3-pip -y
RUN apt install python3-dev -y
RUN python3 -V
RUN pip3 -V
RUN pip3 install --upgrade pip
RUN pip -V


RUN apt install libopus-dev -y
RUN apt install libvpx-dev -y
RUN apt install libffi-dev -y
RUN apt install libssl-dev -y
RUN apt install libopencv-dev -y

#
# -- aiortc --
#
RUN apt install git -y

RUN mkdir /root/work 
WORKDIR /root/work/
RUN git clone https://github.com/jlaine/aiortc.git

RUN pip install aiohttp
RUN pip install aiortc 
RUN pip install opencv-python

#
# ------ yolo v3 ---
#

RUN apt install vim -y
RUN apt install wget -y
WORKDIR /root/work/
RUN git clone https://github.com/pjreddie/darknet.git
WORKDIR /root/work/darknet
RUN make
RUN wget https://pjreddie.com/media/files/yolov3.weights
RUN wget https://pjreddie.com/media/files/yolov3-tiny.weights

RUN ln -s /root/work/darknet/libdarknet.so /usr/lib/libdarknet.so

#-- copy darknet sample ---
WORKDIR /root/work/
RUN git clone https://github.com/mganeko/python3_yolov3.git
RUN cp /root/work/python3_yolov3/darknet-tiny-label.py /root/work/darknet/python/


# --- link ---
RUN ln -s /root/work/darknet/cfg /root/work/aiortc/examples/server/
RUN ln -s /root/work/darknet/data /root/work/aiortc/examples/server/
RUN ln -s /root/work/darknet/yolov3-tiny.weights /root/work/aiortc/examples/server/

#-- copy ---
WORKDIR /root/work/
RUN git clone https://github.com/mganeko/aiortc_yolov3.git
RUN cp /root/work/aiortc_yolov3/server_yolo.py /root/work/aiortc/examples/server/
RUN cp /root/work/aiortc_yolov3/index.html /root/work/aiortc/examples/server/
#COPY server_yolo.py /root/work/aiortc/examples/server/
#COPY index.html /root/work/aiortc/examples/server/


# --- for running --
EXPOSE 8080

WORKDIR /root/work/aiortc/examples/server/
CMD [ "python3", "server_yolo.py" ]

```

# ビルド手順

ターミナルから docker build を実行（イメージ名は適宜付けてください）

```
$ docker build -t mganeko/aiortc-yolov3 -f Dockerfile .
```

# 実行手順

(1) ターミナルから docker run でコンテナを起動

```
docker run -d -p 8001:8080 mganeko/aiortc-yolov3
```

- この例ではホストOSの8001ポートをコンテナの8080ポートに接続しています。
- コンテナの内部では aiortc/examples/server/server_yolo.py を起動しています。

(2) Chromeで http://localhost:8001/ にアクセス
Firefox, Safariでは接続できませんでした

(3) [Use video]をチェック、オプションを選択

今回、次のオプションを追加しました。試したいものを選択してください。

- light blue frame ... YUVで水色のフレームを生成
- draw rectangle with OpenCV ... OpenCVで固定の枠を描画するテスト（物体検出無し）
- object detection with YOLO v3 ... YOLO v3で物体検出、枠とラベルを描画
- detection YOLO v3, rect OpenCV ...  YOLO v3で物体検出、openCVで枠とラベルを描画

<img width="536" alt="aiortc_video_option.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/8ef56961-5bd0-2d74-dd49-4d2f4845509a.png">

枠の描画だけであればリアルタイムに追従できますが、物体検出を行うと途端に重くなり、映像はどんどん遅れて行きます。残念ながらこのやり方では実用にはなりません。

(4) [Start]ボタンをクリック

数秒経つとブラウザとaiortcの接続が確立し、元の映像と加工した映像が交互に表示されます。
object detection with YOLO v3 (YOLO v3で物体検出、枠とラベルを描画)の例はこちらです。

<img width="439" alt="aiortc_yolov3_cellphone.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/95732a9a-04a1-6d9e-6649-f524f67ff34f.png">

今回は yolov3-tiny モデルを使っていますが、それでも1フレームの処理に1秒以上かかります。映像はどんどん遅れてしまうので、数秒以上使うのは困難です。

# 画像の変換

今回のキモは、aiortcのVideoFrameと、darknetのIMAGE間の相互変換です。
YUV形式という名前は聞いたことがあったのですが、今回はじめて使いました。
またPythonとC言語形式のポインター変換もはじめて利用したため、間違いや非効率なこと、あるいはメモリーリークがあるかもしれません。ご指摘いただけると助かります。


## aiortc → darknet

2段階で変換します。

- YUV420 → BGR配列 (OpenCVの画像形式）
- BGR配列 → darknet IMAGE (C言語の構造体、RGBチャネル別のfloat配列を持つ）

```python
from ctypes import *

img = frame_to_bgr(frame)  # YUV420 --> BGR. aiortcで提供されている
dn_image = array_to_image(img) # BGR --> IMAGE structure with float array

# 今回用意した変換関数
def array_to_image(arr):
    arr = arr.transpose(2,0,1)
    c = arr.shape[0]
    h = arr.shape[1]
    w = arr.shape[2]
    arr = (arr/255.0).flatten()
    data = c_array(c_float, arr)  # c_array は darknet のサンプルより
    im = IMAGE(w,h,c,data)
    return im

```


## darknet → aiortc

反対方向も、2段階で変換します。

- darknet IMAGE (C言語の構造体、RGBチャネル別のfloat配列を持つ） → BGR配列 (OpenCVの画像形式）
- BGR配列 → YUV420

```python
from ctypes import *
import numpy

arr = image_to_array(im)  # IMAGE structure --> BGR 
newFrame = frame_from_bgr(arr) # BGR --> YUV420. aiortcで提供されている 

# 今回用意した変換関数
def image_to_array(im):
    size = im.w * im.h * im.c
    data_pointer = cast(im.data, POINTER(c_float))
    arr = numpy.ctypeslib.as_array(data_pointer,shape=(size,))
    arrU8 = (arr * 255.0).astype(numpy.uint8)
    arrU8 = arrU8.reshape(im.c, im.h, im.w)
    arrU8 = arrU8.transpose(1,2,0)
    return arrU8
```

# 枠の描画とラベルの描画

darknetを利用した枠とラベルの描画は、こちらの方法を使っています。

- https://qiita.com/massie_g/items/a2bcfac4fed66b1b0717#python3での実装


OpenCVの場合は次の通りです。

```python
  img = cv2.rectangle(img, (left, top), (right, bottom), (255, 128,0), 3, 4)
  img = cv2.putText(img, name, (left, top), cv2.FONT_HERSHEY_PLAIN, 2, (255, 128, 0), 2, 4)

```

なぜだか文字列変換がうまくできず、 b'label' のように描画されています。

# おわりに

aiortcとYOLO v3を連携させるという目的は達成しましたが、物体検出をリアルタイムで行うのは厳しそうです。原理的にできることと、実際にいまできることの間にはまだ開きがありることを実感しました。

GPUを使ったとしても秒間30フレームをリアルタイムで処理するのは厳しそうなので、実際に利用するには次のような作戦が必要になりそうです。

- 物体検出は毎フレームは行わず、間隔をあけて別スレッドで実行する
- 映像のフレームはリアルタイムで処理する
- 前回の検出結果を、リアルタイムの映像フレームに合成して返す

どうやら、Pythonのマルチスレッド処理を学ばなければならないようです。先は長いです。

(2018.07.17) スレッドを使って映像を止めないようにしました

- [WebRTCの映像を止めないように、別スレッドでYOLO v3の物体検出を行う](https://qiita.com/massie_g/items/20aa3a9d0e04db7e7fab)