YOLOv5で物体検出した結果をPOSTする from Dev Container

# はじめに
YOLOv5を使って動画から物体検出を行い、その結果をPOSTして送ってみたいと思います。

この記事は以下の内容に興味がある人向けに書いています。
1. YOLOV5をとりあえず動かす
1. YOLOV5を組み込んだオリジナルのスクリプトを書く
1. VS Codeの`Dev Containers`で開発環境を用意する（Docker Desktopが有料になってしまったのでRancher Desktopで）
1. モック環境(httpbin)を使ってテストする

では早速、YOLOv5のREADMEを読みながら作業を進めます。

https://github.com/ultralytics/yolov5

インストール手順は非常にシンプルでCloneしてから必要なPythonモジュールをインストールするだけです。。※READMEより抜粋
```
git clone https://github.com/ultralytics/yolov5  # clone
cd yolov5
pip install -r requirements.txt  # install
```

ご自身の好きな環境で作業を行なっていただくのが良いと思いますが、私は~~M1 Macを使っておりPython周りの環境構築がめんど~~普段VS Codeを使っていることと、手間をかけずにコンテナで用意したい、コンテナ環境とはいえローカル環境を触る感じで作業したいと思いますので、[Visual Studio CodeのDev Containers](https://code.visualstudio.com/docs/devcontainers/containers)を使おうと思います。

コンテナを用意して、そこにClone、必要なモジュールのインストールなどしていきます。ローカル環境で作業を進めることもできますので、開発環境の用意の手順は必要に応じて読み飛ばしてください。

> この機能を使うには、Docker daemonが動作している必要があります。私はDocker Desktopが有料化してまったので[Rancher Desktop](https://rancherdesktop.io/)を使っています。Rancher Desktopを使う場合、Container Engineがcontainerdになっていると動作しないのでdockerd(moby)を選択するようにしてください。また、[Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)は拡張機能です。

## 1. 開発環境の用意

開発用コンテナを用意していきます。選択肢を選んでいくと、次の項目へ遷移していきます。
開発環境の用意の手順に関してはVS Code(+Dev Containers)、Rancher Desktopの準備は済んでいる前提で書いています。

1. VS Codeを開き左下の赤線部分のアイコンをクリックする
![Screenshot 2022-11-15 at 16.30.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/6d5e0711-61c6-f2d8-1ce4-b968a164e3ea.png)

2. `Dev Containers`の`Clone Repository in Container Volume...`をクリックする
![Screenshot 2022-11-15 at 16.55.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/b33cb766-5ab2-5aab-ebfe-83f1af6f3298.png)

3. YOLOv5のリポジトリのURLをペーストし、`Clone a repository from GitHub in a Container Volume`をクリックする
![Screenshot 2022-11-15 at 17.32.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/cb7cd603-dee9-5000-4325-80a30667fa7b.png)

4. YOLOv5のリポジトリ名を入力して検索し、選択する
![Screenshot 2022-11-15 at 17.33.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/199b74c9-a3f6-4e7a-c459-bfbe098afdf7.png)

5. `master`ブランチを選択する
![Screenshot 2022-11-15 at 17.33.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/40238870-b9c6-51fb-b1e0-c5e37dcf06a4.png)

>ここまで選択するとリポジトリの設定は終わり、VS Codeのウィンドウが開き直されてコンテナの設定に遷移します。

3. Dev Containerの設定を行う

YOLOv5を使うにあたってPythonを使いたいので`Python 3`にしました。
![Screenshot 2022-11-15 at 16.56.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/b6a1dc87-a57c-7e9d-7e05-d66f2c2817a3.png)

Python 3.7.0以上であれば良いです。今回は`3.10`にしました。
![Screenshot 2022-11-15 at 16.56.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/2506c288-a67f-1287-70ec-baaeb686f252.png)
Node.jsは特に必要ないので`None`にしました。
![Screenshot 2022-11-15 at 16.57.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/cb66612f-9095-1f09-3bba-fab862988b12.png)
追加で入れたいものも特にないのでそのまま`OK`しました。
![Screenshot 2022-11-15 at 16.57.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/226917a3-bbe1-6bf3-ae89-727d6b687fd2.png)

設定が終わると少しのインターバルの後にコンテナが起動し、ウィンドウが再び開き直されます。
![Screenshot 2022-11-15 at 16.57.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/d6535ffe-9cbb-b269-30ff-7a900850f684.png)

左側にYOlOv5のリポジトリからCloneした内容と`.devcontainer`のフォルダ（設定ファイルであるdevcontainer.jsonを含む）があることが確認できます。ローカル環境に存在しているかのようにファイルの中身などを確認できます。

また、ウィンドウ上部に`yolov5[Dev Container: Python3@rancher-desktop]`と表示されていることも確認できます。

これで準備OKです。
余談ですが、OSはDebianです。
```
$ cat /etc/os-release 
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
NAME="Debian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
```

## 2. 必要なPythonモジュールのインストール

手順といってもこれだけです。
```sh
pip install -r requirements.txt
```

## 3. YOLOv5の動作確認
インストールが完了したので動作するか確認します。
READMEを確認すると`detect.py`というスクリプトで実際に試せると書いてあります。※READMEより抜粋
```
python detect.py --source 0  # webcam
                          img.jpg  # image
                          vid.mp4  # video
                          screen  # screenshot
                          path/  # directory
                          'path/*.jpg'  # glob
                          'https://youtu.be/Zgi9g1ksQHc'  # YouTube
                          'rtsp://example.com/media.mp4'  # RTSP, RTMP, HTTP stream
```

Cloneした中に`data/images`というディレクトリがあり、そこにサンプル画像があるのでこれを使って試してみます。
ジダンの画像とバスの画像があります。今回はジダンの画像を使ってみます。
<img width="400" alt="元画像" src="https://ultralytics.com/images/zidane.jpg">
```
$ python detect.py --source data/images/zidane.jpg 
- 略 -  
ImportError: libGL.so.1: cannot open shared object file: No such file or directory
```
実行したところエラーになりました。
OpenCVに必要なパッケージが足りないようなのでインストールします。
```
sudo apt install -y libgl1-mesa-dev
```
気を取り直してもう一度試します。
```
$ python detect.py --source data/images/zidane.jpg 
/home/vscode/.local/lib/python3.10/site-packages/torchvision/io/image.py:13: UserWarning: Failed to load image Python extension: 
  warn(f"Failed to load image Python extension: {e}")
detect: weights=yolov5s.pt, source=data/images/zidane.jpg, data=data/coco128.yaml, imgsz=[640, 640], conf_thres=0.25, iou_thres=0.45, max_det=1000, device=, view_img=False, save_txt=False, save_conf=False, save_crop=False, nosave=False, classes=None, agnostic_nms=False, augment=False, visualize=False, update=False, project=runs/detect, name=exp, exist_ok=False, line_thickness=3, hide_labels=False, hide_conf=False, half=False, dnn=False, vid_stride=1
YOLOv5 🚀 v6.2-240-gabbfd69 Python-3.10.8 torch-1.13.0 CPU

Fusing layers... 
YOLOv5s summary: 213 layers, 7225885 parameters, 0 gradients
image 1/1 /workspaces/yolov5/data/images/zidane.jpg: 384x640 2 persons, 2 ties, 192.1ms
Speed: 0.3ms pre-process, 192.1ms inference, 1.1ms NMS per image at shape (1, 3, 640, 640)
Results saved to runs/detect/exp
```
`runs/detect/exp`に結果が保存されているとのことなので見てみます。
![zidane.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/51c1fdd9-e979-7baf-473f-66e5982efea7.jpeg)
推論結果が反映された画像が出力されていました。

> Warningが出ていますが本内容と関係がないのでここでは無視します

## 4. 検出結果をPOSTする

単に検出だけするよりも、その結果を他システムに渡せた方が役立てやすいと思いますのでその実装をして行きます。

REAMEを確認すると検出までのサンプルが記載されています。
```python
import torch

# Model
model = torch.hub.load('ultralytics/yolov5', 'yolov5s')  # or yolov5n - yolov5x6, custom

# Images
img = 'https://ultralytics.com/images/zidane.jpg'  # or file, Path, PIL, OpenCV, numpy, list

# Inference
results = model(img)

# Results
results.print()  # or .show(), .save(), .crop(), .pandas(), etc.
```
モデルを読み込み→画像を取得→推論→結果の表示とすごくシンプルです。これをベースにスクリプトを作ってみました。また、先ほどの動作確認では画像でしたが今度は動画にします。コンテナで作業している都合上、Webカメラの映像ではなくこちらのYoutubeの動画を使います。

<iframe width="560" height="315" src="https://www.youtube.com/embed/Zgi9g1ksQHc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

検出結果の送信先には[httpbin](https://httpbin.org) を使います。

https://qiita.com/sameyasu/items/adacceb8a1bee893599b

HTTPリクエストをするとレスポンスを返してくれるサービスです。

Youtubeの動画から物体を検出し、その結果をhttpbinに送信します。
> 送信しすぎても邪魔なのでこのスクリプトは5秒に一度POSTします。

```python:yolov5_object_detector.py
import torch
import cv2
import pafy
import json
import requests
import time
import pprint

MOVIE_URL = 'https://youtu.be/Zgi9g1ksQHc'
SENDING_INTERVAL_SEC = 5
EXTERNAL_SERVICE_ENDPOINT = 'https://httpbin.org/post'

def post_to_external(detections):
    headers = {
        'content-type': 'application/json'
    }
    message = {
        'camera_id': 'yolov5_object_detection_camera',
        'detections': detections
    }
    response = requests.post(EXTERNAL_SERVICE_ENDPOINT, headers=headers, data=json.dumps(message))
    print('-----')
    pprint.pprint(response.json())

def main():
    model = torch.hub.load('ultralytics/yolov5', 'yolov5s')
    movie = pafy.new(MOVIE_URL)
    best = movie.getbest()
    cap = cv2.VideoCapture(best.url)
    last_sent_time = time.time()
    while True:
        ret, frame = cap.read()
        result = model(frame)
        result.render()
        dict_detections = result.pandas().xyxy[0].to_dict(orient='records')
        results = []
        for detection in dict_detections:
            result = {
                'confidence': round(detection['confidence'], 2),
                'label': detection['name'],
                'xmin': int(detection['xmin']),
                'xmax': int(detection['xmax']),
                'ymin': int(detection['ymin']),
                'ymax': int(detection['ymax'])
            }
            results.append(result)
        now = time.time()
        if len(results) != 0:
            if now - last_sent_time > SENDING_INTERVAL_SEC:
                post_to_external(results)
                last_sent_time = time.time()
        key = cv2.waitKey(1)
        if key == 27:
            break

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

実行してみると、httpbinからのレスポンスが表示されます。httpbinはこちらがPOSTした際の内容を含めてレスポンスを返してくれます。
```
{'args': {},
 'data': '{"camera_id": "yolov5_object_detection_camera", "detections": '
         '[{"confidence": 0.85, "label": "person", "xmin": 0, "xmax": 419, '
         '"ymin": 345, "ymax": 535}, {"confidence": 0.76, "label": "cup", '
         '"xmin": 8, "xmax": 167, "ymin": 292, "ymax": 368}, {"confidence": '
         '0.54, "label": "laptop", "xmin": 165, "xmax": 1198, "ymin": 48, '
         '"ymax": 653}]}',
 'files': {},
 'form': {},
 'headers': {'Accept': '*/*',
             'Accept-Encoding': 'gzip, deflate',
             'Content-Length': '335',
             'Content-Type': 'application/json',
             'Host': 'httpbin.org',
             'User-Agent': 'python-requests/2.28.1',
             'X-Amzn-Trace-Id': 'Root=1-63748bc5-5bf64a0141077caa1215f19b'},
 'json': {'camera_id': 'yolov5_object_detection_camera',
          'detections': [{'confidence': 0.85,
                          'label': 'person',
                          'xmax': 419,
                          'xmin': 0,
                          'ymax': 535,
                          'ymin': 345},
                         {'confidence': 0.76,
                          'label': 'cup',
                          'xmax': 167,
                          'xmin': 8,
                          'ymax': 368,
                          'ymin': 292},
                         {'confidence': 0.54,
                          'label': 'laptop',
                          'xmax': 1198,
                          'xmin': 165,
                          'ymax': 653,
                          'ymin': 48}]},
 'origin': '61.119.203.193',
 'url': 'https://httpbin.org/post'}
```

`data`、`json`というパラメーターの中に検出結果が含まれていることが確認できます。

# まとめ
Dev Containersを使う手順の説明があったので少し長くなりましたが、YOLOv5で推論するだけなら、
```
git clone https://github.com/ultralytics/yolov5  # clone
cd yolov5
pip install -r requirements.txt  # install
```
```
python detect.py --source data/images/zidane.jpg 
```
で終わりです。とても手軽ですね。
今回のようにどこかにPOSTする処理を加えるようなアレンジをしたい際もベースがシンプルなのでやりやすいです。
```python
import torch

# Model
model = torch.hub.load('ultralytics/yolov5', 'yolov5s')  # or yolov5n - yolov5x6, custom

# Images
img = 'https://ultralytics.com/images/zidane.jpg'  # or file, Path, PIL, OpenCV, numpy, list

# Inference
results = model(img)

# Results
results.print()  # or .show(), .save(), .crop(), .pandas(), etc.
```