DeepStream SDKでリアルタイム動画認識からのLINE通知

# DeepStream SDKで高速動画認識からのLINE通知

この記事は，ドコモアドベントカレンダー2019 7日目の記事です。

NTTドコモ サービスイノベーション部の酒井と申します。業務ではDeep Learningを用いた画像認識エンジンの研究開発、サービス化に取り組んでいます。今回は、GPUを使った動画認識高速化ツール「DeepStream SDK」を用いて、動画からの車検出を高速で行うとともに、結果をLINEに通知してみました。AIカメラ的な物を作って、監視カメラで人を検知したり、カメラ映像から違法駐車の検出などを行うなどに使える感じです。

![動画gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320406/ac5bcb47-2ee3-a11c-6c49-ce5405a6e45d.gif)


Web上であまり事例の見つけられなかったDeepStream SDKとAMQPを用いたメッセージングにもチャレンジしました。

![workflow.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320406/b972b1e7-af1c-757d-d45d-da514f90d85e.jpeg)


## 動機

近年、画像認識技術でのDeep Learning利用が広がるに連れ、動画データの画像認識も徐々に盛り上がって来ています。しかし、Deep Learningを用いた動画認識は、通常の画像認識よりも、多くのマシンスペックを必要とし、処理時間が遅くなりがちです。

そんなとき、NVIDIA社の[DeepSTtream SDK](https://developer.nvidia.com/deepstream-sdk)を使うと、動画認識を高速でGPUを使って動画処理を高速化できるとのこと。dockerでも使えるようになり、導入が簡単になったので試してみました。

## DeepStream SDK

Deep Learningの推論(+学習)では、高速化のためにGPUがよく使われます。DeepStream SDKは、GPUを用いて動画処理を高速に行うためのSDKです。動画処理に必要な各種機能を、ストリーム処理で一般的なGstreamerのプラグインの形で提供しています。

- GPUや、エッジGPUデバイスの専用ハードウェアを用いて、動画のエンコード/デコードを高速化
- Deep Learningを用いた物体検出、画像分類を、GPUおよびTensorRT[^1]を用いて高速化
- トラッキング機能も提供
- 画像認識結果を、Kafka、MQTT、AMQPなどを用いて、送信可能


## 方法

### 環境

[NVIDIA docker 2.0](https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(version-2.0))の使えるGPUサーバを準備しました。

- OS: Ubuntu 18.04
- nvidia-driver: 418.67
- docker: 19.03.4
- docker-compose: 1.24.1
- nvidia-docker: 2.0.3

gpuがdockerで[nativeサポート](https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support))されましたが、docker-composeではGPUを指定できないようなので、古いruntime式の指定方法を採用しました。

### ファイル構成

```txt
deepstream-line
├── docker
│   ├── Dockerfile
│   ├── requirements.txt
│   └── src
│       └── receive.py
├── docker-compose-amqp.yml
├── docker-compose.yml
├── rabbitmq
├   ├── custom_definitions.json
├   └── rabbitmq.conf
└── videos
    └── test.h264
```

動画データは、フリーの動画素材サイト[Mixkit](https://mixkit.co/free-stock-video/)から、
[Chinatown street at night](https://mixkit.co/videos/chinatown-street-at-night-2012/)を使わせていただいています。

h264のストリームにしておく必要があるので、ffmpegを用いて変換しておきます。

```bash
ffmpeg -i ./2012-1080.mp4  -vcodec copy -an -bsf:v h264_mp4toannexb videos/test.h264
```

### AMQPサーバの起動

DeepStream SDKは、認識結果をメッセージとしてKafka, MQTT, AMQPなどに送信することができます。
今回はdockerを使って、[RabbitMQ](https://www.rabbitmq.com/)を立ち上げることにしました。
RabbitMQはPub/Sub型のメッセージングサービスで、以下のような要素で構成されています。

![rabbitmq.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320406/3cfb4900-6a9b-9763-4e86-ec3abfe5dbe4.jpeg)

- producer: メッセージを送る側。今回は、DeepStream SDKがこれに当たる
- exchange: 送られてきたメッセージを適当なqueueに割り振る
- queue: キュー
- consumer: メッセージをqueueに取りに行って利用する。今回は、LINEに通知を送るpythonスクリプト

AMQPの設定ファイルを用意します。ユーザ、パスワードはともにguestにしました。

```json:rabbitmq/custom_definitions.json
{
    "rabbit_version": "3.8.0",
    "users": [{
        "name": "guest",
        "password_hash": "CV/8dVC+gRd5cY08tFu4h/7YlGWEdE5lJo4sn4CfbCoRz8ez",
        "hashing_algorithm": "rabbit_password_hashing_sha256",
        "tags": "administrator"
    }],
    "vhosts": [{
        "name": "/"
    }],
    "permissions": [{
        "user": "guest",
        "vhost": "/",
        "configure": ".*",
        "write": ".*",
        "read": ".*"
    }],
    "topic_permissions": [],
    "parameters": [],
    "global_parameters": [{
        "name": "cluster_name",
        "value": "rabbit@rabbitmq"
    }],
    "policies": [],
    "queues": [{
        "name": "test",
        "vhost": "/",
        "durable": true,
        "auto_delete": false,
        "arguments": {
            "x-queue-type": "classic"
        }
    }],
    "exchanges": [],
    "bindings": [{
        "source": "amq.topic",
        "vhost": "/",
        "destination": "test",
        "destination_type": "queue",
        "routing_key": "deepstream",
        "arguments": {}
    }]
}
```

queuesで、queueの名前をtestと設定しています。また、exchangeはデフォルトで設定されているamq.topicを使うこととし、bindingsで、"deepstream"というtopicのついたメッセージが来た際に、test queueにルーティングするようにしています。

前記jsonを読み込むように、confファイルも準備します。

```conf:rabbitmq/rabbitmq.conf
loopback_users.guest = false
listeners.tcp.default = 5672
management.tcp.port = 15672
management.load_definitions = /etc/rabbitmq/custom_definitions.json
```

これで、先ほどのjsonを読み込みつつ、port 15672でメッセージのやり取りができます。

docker-composeを用いて、RabbitMQを起動します。RabbitMQの公式ベースイメージを取ってきつつ、volumesオプションを使って、先ほどのconf/jsonをコンテナ内から見えるようにマウントします。

```yaml:docker-compose-amqp.yml
version: "2.3"
services:
  rabbitmq:
    image: rabbitmq:3.8-management
    hostname: rabbitmq
    container_name: rabbitmq
    expose: #他のコンテナ向けの公開
      - "5672"
      - "15672"
    ports: #
      - "5672:5672"
      - "15672:15672"
    volumes:
      - ./rabbitmq:/etc/rabbitmq
networks:
  default:
```

また、ネットワークを定義することで、後々DeepStream SDKやconsumerのコンテナから接続できるようにしています。以下のコマンドで起動します。


```bash
docker-compose -f docker-compose-amqp.yml up -d
```

### DeepStream SDKの準備

DeepStream SDKは、[NVIDIA GPU CLOUD](https://ngc.nvidia.com/)で提供されている、公式イメージを使うことにします。

使うイメージは[nvcr.io/nvidia/deepstream:4.0.1-19.09-samples](https://ngc.nvidia.com/catalog/containers/nvidia:deepstream/tags)です。

あらかじめpullしておきます。

```bash
docker pull nvcr.io/nvidia/deepstream:4.0.1-19.09-samples
```

このイメージには、コンパイル済みのDeepStream SDK、サンプル、TensorRT、Gstreamerなど、必要なものがすべて入っています。今回はこのdocker imageに含まれるサンプルアプリのなかで、[DeepStream Test 4](https://developer.nvidia.com/deepstream-faq#sample_apps)を使います。これは、車/人の検出・認識を行ったうえで、認識結果を30フレームごとにメッセージで送信できるというアプリです。

> Directory: <DS installation dir>/sources/apps/sample_apps/deepstream-test4
> Description: This builds on top of the deepstream-test1 sample for single H.264 stream - filesrc, decode, nvstreammux, nvinfer, nvosd, renderer to demonstrate the use of "nvmsgconv" and "nvmsgbroker" plugins in the pipeline for IOT connection. For test4, user have to modify kafka broker connection string for successful connection. Need to setup analytics server docker before running test4. The DeepStream Analytics Documentation has more information on setting up analytics servers.

以下のコマンドでREADMEの確認、Usageの確認が可能です。

```bash
docker run --gpus 0 --rm -it \
    nvcr.io/nvidia/deepstream:4.0.1-19.09-samples \
    cat /root/deepstream_sdk_v4.0.1_x86_64/sources/apps/sample_apps/deepstream-test4/README
# READMEが表示される
docker run --gpus 0 --rm -it \
    nvcr.io/nvidia/deepstream:4.0.1-19.09-samples \
    /opt/nvidia/deepstream/deepstream-4.0/bin/deepstream-test4-app
# /opt/nvidia/deepstream/deepstream-4.0/bin/deepstream-test4-app \
# -i <H264 filename> -p <Proto adaptor library> --conn-str=<Connection string
```

READMEによると、`--proto-lib`、`--conn-str`、`--topic `のオプションを設定することでAMQPが使えるようになります。

>  1.Use --proto-lib or -p command line option to set the path of adaptor library.
>    Adaptor library can be found at /opt/nvidia/deepstream/deepstream-<version>/lib
>
>    kafka lib           - libnvds_kafka_proto.so
>    azure device client - libnvds_azure_proto.so
>    AMQP lib            - libnvds_amqp_proto.so
>
>  2.Use --conn-str command line option as required to set connection to >backend server.
>    For Azure           - Full Azure connection string
>    For Kafka           - Connection string of format:  host;port;topic
>    For Amqp            - Connection string of format:  host;port;username. Password to be provided in cfg_amqp.txt
>
>  3.Use --topic or -t command line option to provide message topic (optional).
>    Kafka message adaptor also has the topic param embedded within the connection string format
>    In that case, "topic" from command line should match the topic within connection string

[AMQPサーバの起動](#AMQPサーバの起動)において、hostはrabbitmq、portは15672、userはguestとしたので、`-c rabbitmq;5672;guesttopicはdeepstream -c cfg_amqp.txt`とすればよいことになります。

`libnvds_amqp_proto.so`はdockerイメージ内の`/opt/nvidia/deepstream/deepstream-4.0/lib/libnvds_amqp_proto.so`に格納されています。


### Consumerの準備

AMQPサーバからメッセージを取得し、LINEに通知を送るコンテナを準備します。
LINEに通知を送るために、LINE Notifyを使います。
これは、Webサービスと連携すると、LINEが提供する公式アカウント"LINE Notify"から通知が届く、というものです。
この「Webサービス」に当たるものをpythonで書きます。
専用のTOKENを取得しておき、TOKENを使って認証しつつ特定のURLにメッセージをPOSTすることで、通知が送れます。
今回は、[こちらの記事](https://qiita.com/aoyahashizume/items/13848b013daa18f6461b)を参考に、TOKENを取得しておきました。取得したTOKENは[実行](#実行)の所で使いますので、どこかにメモしておきます。

```python:docker/src/receive.py
import pika
import json
import requests
import os

LINE_URL = "https://notify-api.line.me/api/notify"
line_token = os.environ['LINE_TOKEN'] #環境変数からTOKENを持ってくる
message_format = '''
時刻: {time:s}
場所: {place:s}
カメラ: {camera:s}
検出された物体: {object:s}
'''


def main():
    # mqttに接続する
    credentials = pika.PlainCredentials('guest', 'guest')
    connect_param = pika.ConnectionParameters(
        host='rabbitmq',  #docker-compsoe-amqpでcontainerにつけたホスト名
        credentials=credentials
    )
    connection = pika.BlockingConnection(connect_param)
    channel = connection.channel()

    # line認証用のtoken
    line_headers = {"Authorization": "Bearer {}".format(line_token)}

    for method_frame, properties, body in channel.consume(
        'test',
        inactivity_timeout=30  # 30秒新規メッセージがなければbreakする
    ):
        if method_frame is None:
            break

        message = json.loads(body)
        if 'vehicle' in message['object'].keys():
            obj_data = '{} {} {}'.format(
                message['object']['vehicle']['make'],
                message['object']['vehicle']['model'],
                message['object']['vehicle']['color'],
            )
        elif 'person' in message['object'].keys():
            obj_data = 'Person {} {}'.format(
                message['object']['person']['age'],
                message['object']['person']['gender']
            )
        else:
            obj_data = ''
        payload = {
            "message": message_format.format(
                time=message['@timestamp'],
                place='{}_{}({})'.format(
                    message['place']['id'],
                    message['place']['name'],
                    message['place']['type'],
                ),
                camera=message['sensor']['id'],
                object=obj_data,
            )
        }
        r = requests.post(
            LINE_URL,
            headers=line_headers,
            params=payload
        )
        channel.basic_ack(method_frame.delivery_tag)

    # consumerをキャンセルし、pending中のメッセージがあればreturn
    requeued_messages = channel.cancel()
    print('Requeued %i messages' % requeued_messages)
    connection.close()


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

`payload =`のところで、DeepStream SDKからAMQP経由で送られてきたメッセージを処理しています。
DeepStream Test 4からのメッセージがjson形式で30フレームに一回送信されます(DeepStream Test 4のせっていち)。
メッセージには、検出されたオブジェクトのうちの1つの情報が以下の形式で格納されています。
placeに格納される撮影場所情報、cameraに格納されるカメラ情報、時刻、objectの情報を取り出すこととしました。

```json
{
    "messageid": "a8c57c62-5e25-478d-a909-3ab064cbf11f",
    "mdsversion": "1.0",
    "@timestamp": "2019-11-17T13:42:39.807Z",
    "place": {
        "id": "1",
        "name": "XYZ",
        "type": "garage",
        "location": {
            "lat": 30.32,
            "lon": -40.55,
            "alt": 100.0
        },
        "aisle": {
            "id": "walsh",
            "name": "lane1",
            "level": "P2",
            "coordinate": {
                "x": 1.0,
                "y": 2.0,
                "z": 3.0
            }
        }
    },
    "sensor": {
        "id": "CAMERA_ID",
        "type": "Camera",
        "description": "\"Entrance of Garage Right Lane\"",
        "location": {
            "lat": 45.293701447,
            "lon": -75.8303914499,
            "alt": 48.1557479338
        },
        "coordinate": {
            "x": 5.2,
            "y": 10.1,
            "z": 11.2
        }
    },
    "analyticsModule": {
        "id": "XYZ",
        "description": "\"Vehicle Detection and License Plate Recognition\"",
        "source": "OpenALR",
        "version": "1.0",
        "confidence": 0.0
    },
    "object": {
        "id": "-1",
        "speed": 0.0,
        "direction": 0.0,
        "orientation": 0.0,
        "vehicle": {
            "type": "sedan",
            "make": "Bugatti",
            "model": "M",
            "color": "blue",
            "licenseState": "CA",
            "license": "XX1234",
            "confidence": 0.0
        },
        "bbox": {
            "topleftx": 585,
            "toplefty": 472,
            "bottomrightx": 642,
            "bottomrighty": 518
        },
        "location": {
            "lat": 0.0,
            "lon": 0.0,
            "alt": 0.0
        },
        "coordinate": {
            "x": 0.0,
            "y": 0.0,
            "z": 0.0
        }
    },
    "event": {
        "id": "4f8436ab-c611-4257-8b83-b9134c6cab0d",
        "type": "moving"
    },
    "videoPath": ""
}
```

上記consumerを実行するコンテナイメージはDockerfileを用いて作ります。python3を導入したうえで、pipを用いて必要なpython packageを導入しています。最後にソースコードをコピーして終わりです。

```Dockerfile:docker/Dockerfile
FROM ubuntu:18.04

RUN apt-get update \
    && apt-get install -y python3-pip python3-dev \
    && cd /usr/local/bin \
    && ln -s /usr/bin/python3 python \
    && pip3 install --upgrade pip \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt /tmp/requirements.txt
RUN pip3 install -r /tmp/requirements.txt

COPY src /opt/src
WORKDIR /opt/src

CMD ["python3", "./receive.py"]
```

```txt:requirements.txt
pika
requests
```

### 実行

docker-composeを用いて、DeepStream SDKとconsumerを立ち上げます。

```yaml:docker-compose.yml
version: "2.3"
services:
  deepstream:
    image: nvcr.io/nvidia/deepstream:4.0.1-19.09-samples
    runtime: nvidia
    hostname: deepstream
    container_name: deepstream
    command: >
      /opt/nvidia/deepstream/deepstream-4.0/bin/deepstream-test4-app
      -i /root/videos/test.h264
      -p /opt/nvidia/deepstream/deepstream-4.0/lib/libnvds_amqp_proto.so
      -c cfg_amqp.txt
      -t deepstream
      --conn-str rabbitmq;5672;guest
    cap_add:
      - SYSLOG
    working_dir: /root/deepstream_sdk_v4.0.1_x86_64/sources/apps/sample_apps/deepstream-test4
    networks:
      - deepstream-line_default
    volumes:
      - ./videos:/root/videos
      - /tmp/.X11-unix:/tmp/.X11-unix
    environment:
      - DISPLAY=$DISPLAY
  consumer:
    build: ./docker
    hostname: consumer
    container_name: consumer
    environment:
      LINE_TOKEN: "XXX"
    networks:
      - deepstream-line_default
networks:
  deepstream-line_default:
    external: true
```

```LINE_TOKEN: "XXXXXXXX"```のところに、取得したTOKENを貼り付けます。これで、自分のLINEに通知が届くようになります。

以下を実行すると処理が開始されます。

```bash
xhost + # dockerから結果のdisplay表示ができるように
docker-compose build # consumerのbuild処理
docker-compose up -d
```

これはdisplayのある環境を想定しているので、サーバなどで実行するには、若干修正[^2]が必要です。

## 結果

docker-compose upすると、以下のような形で動画の認識処理が立ち上がり、LINEに通知が飛んできます。

![result_all.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320406/4012ad49-10a5-76fa-45d2-6d1aa450d97f.gif)

上はPC版のLINEですが、スマホだと以下のような感じで通知が届きます。

![Screenshot_20191206-212712_LINE.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320406/7cd843bd-bb95-48da-096e-213d74d8f8b5.jpeg)


## さいごに

DeepStream SDKを用いて、高速動画認識を行ってLINE通知を行う方法を紹介しました。今回は普通のサーバを用いましたが、[deepstream-l4t](https://ngc.nvidia.com/catalog/containers/nvidia:deepstream-l4t)コンテナを使うことで、jetson等のエッジGPU用のマシンでも同様の処理が可能なはず。AIカメラ的なものを自作する夢が広がります。

また、今回はDeepStream SDK側のアプリにおいて好みの認識モデルを使ったり、出力するデータの形式を変更したりする取り組みはできませんでした。また機会があれば紹介できればと思います。

最後までお読みいただきありがとうございました。


[^1]: NVIDIA製のGPU上でのDeep Learning推論処理を高速化するためのライブラリ。以前、[TensorRT 5.0使ってみた](https://qiita.com/dcm_sakai/items/0e13e2917adf55e92745)、[TensorRT 5.0.6とJETSON NANOで推論の高速化](https://qiita.com/104ki/items/17434efa7ce045f767dd)などの記事を書いてます。
[^2]: displayがないサーバなどで実行する場合は、docker-compose.ymlの`deepstream`から`environment`を抜き、`command`に`--no-display`オプションをつければ実行できます。