指定した車両をトラッキング

<!--
project folder : ActiveCamera / tried / object_detection / 
公開用のhtml: publication / tracking.html
事前に動画からpngを抜き出す: publication / video_to_png.html
-->

# 0. 概要

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

- この動画の例の場合、シケインに入る1,2,3番目の車両、わざと少し飛ばして7,8番目の車両といった5台の車両を追いかけて画面下に拡大表示します。
- 車両の検出にはカスタムデータで再学習した**yolox-x**、トラッキングには**motpy**を使わさせて頂いています。

# 1. 処理の概要

**yolox-xによる車両の検知**

https://qiita.com/toyohisa/items/147099e52bd138669a6f

↑ で作成したyolox-xを用いて以下のような形式の動画のフレームごとの物体検出結果のjsonファイルを作成します。但し上記の例では"ゼッケン１の車両"と"他の車両"を区別する2クラスとしていますが、ここでは"車両"という1クラスのみに変えています。

**motpyによるトラッキング**

つぎにmotpyの**examples/detect_and_track_in_video.py**ファイルを編集して、元ではtorchvisionの物体検出を呼び出すようになっているところを上記のyolox-xの検出結果のjsonファイルを読み込むようにします。

**トラッキング結果の表示**

最後に上記のトラッキング結果を使って動画再生にトラッキングした物体を枠で表示したり、その部分を抽出して拡大表示するインターフェースを作成します。今回は、javascriptで作成しています。

# 2. yoloxの推論結果をjsonファイルに出力する

yoloxのcolaboratoryへのインストール、カスタムデータで再学習については、[ゼッケン１のバイクを捜せ](https://qiita.com/toyohisa/items/147099e52bd138669a6f)をご参照ください。ここでは学習済みの重みファイルが既にあり、そこから動画を読み込んで物体検知の結果をjsonファイルに出力するところまでを記述します。

yoloxのインストールフォルダの**tools/demo.py**を編集します。

```python:yolox/tools/demo.py
# 227行目あたりの imageflow_demo関数内のwhileの前
def imageflow_demo(predictor, vis_folder, current_time, args):
    # ....
    stored_outputs = [] ''' ← 出力結果の保存先 '''
    while True:
        # ...
        if ret_val:
            # ...
            ''' ↓ 検出物体なしのときは空のリストを追加、またbboxは元画像のサイズに変換して保存する '''
            if outputs[0]==None:
                stored_outputs.append([])
            else:
                output_np = outputs[0].cpu().detach().numpy().copy()
                bbox = output_np[:,0:4]/img_info["ratio"]
                import numpy as np
                stored_outputs.append(np.hstack((bbox,output_np[:,4:])).tolist())
            ''' ↑ 追加はここまで '''
            # ...

    # 上のwhileの終了後
    ''' ↓ stored_outputsをjsonファイルに保存 '''
    import json
    stored_outputs_filename = "stored_yolox_outputs.json"
    with open(stored_outputs_filename,"wt") as f:
        json.dump(stored_outputs,f)
        print(f"saved to {stored_outputs_filename}")
    ''' ↑ 追加はここまで '''
# ...
```
青いコメントのある行が追加した行です。yoloxのdemo.pyは、動画を入力データとした場合、imageflow_demo関数が呼び出されます。その中で動画をキャプチャしてフレームごとに推論処理を実施するわけですが、その結果を得るたびにstored_outputsに保存しておきます。
結果を保存する際には、検出した物体の枠であるbboxを元画像のサイズの数値に変換しておきます。ちなみにyolox-xは入力画像を640x640に変換して機械学習モデルの入力としているようです。
そしてすべてのフレームに対する推論処理が終わったwhileの後で、stored_outpusをjsonファイルに保存します。

**yoloxの出力フォーマット**
```python: yoloxの出力フォーマット
[
    tensor([
        '''[xmin,ymin,xmax,ymax,object conf,class conf,class]'''
        [459.5991, 149.8516, 511.1499, 178.0078, 0.9905, 0.9551, 0.0000],
        [168.4778, 122.5135, 194.2475, 155.4523, 0.9906, 0.9365, 0.0000]
    ])
]
```
上の例は2つの物体を検出したときの例です。7個の要素で作られるリストは、検出物体を囲む枠の左上(xmin,ymin)と右下(xmax,ymax)の座標と、その枠に対する何かしらの物体があるという確率(object conf)、その物体はクラス値が示す物体である確率(class conf)、そしてクラス値(class)です。単純にこの物体の確信度を求める場合、object conf×class confで計算します。

```python:編集したdemo.pyのcolaboratory上での実行
%run tools/demo.py video -f ../myexp_yolox_x.py -c YOLOX_outputs/myexp_yolox_x/best_ckpt.pth --path douga.mp4 --conf 0.25 --nms 0.45 --save_result
```
colaboratory上にインストールしたyoloxで上記の編集したdemo.pyを実行する場合には、上記のようなコマンドをcolaboratory上で実行します。colaboratory上のyoloxについては、詳しくは[ゼッケン１のバイクを捜せ](https://qiita.com/toyohisa/items/147099e52bd138669a6f)をご参照ください。

上記を実行すると、現階層にstored_yolox_outputs.jsonファイルが出力されます。


# 3. motpyのトラッキング結果をjsonファイルに出力する

## 3.1. motpyをcolaboratoryにインストールする

ここではmotpyをgoogle colaboratoryで動作させるようにしたときのメモを残します。

```python:Google Driveのマウント
# google drive のマウント
current_folder = "/content/drive/MyDrive/Colab Notebooks/yolox"
from google.colab import drive
drive.mount('/content/drive')
%cd $current_folder
```

インストール先を永続的に利用できるGoogle Driveにするために、最初にマウントします。

```python:この記事作成時の各種バージョンの確認
import numpy as np
import torch
import torch.onnx

# 各種のバージョンを確認する。
!python --version
print(f"numpy {np.__version__}")
print(f"pytorch {torch.__version__}")
```
>Python 3.10.12
numpy 1.22.4
pytorch 2.0.1+cu118

この記事の作成時(2023.08.07)時点のColaboratoryの各種モジュールのバージョンです。

```python:motpyのインストール
# motpyのインストール
!git clone https://github.com/wmuron/motpy
%cd motpy
!make install-develop
!make test
```
>tests/test_metrics.py ...                                                [ 12%]
tests/test_model.py ...                                                  [ 24%]
tests/test_tracker.py ...................                                [100%]

上記のような出力がされていれば、インストールは成功しているようです。

```python:motpyで使用するモジュールのインストール
# motpyで使用するモジュールのインストール
!pip install fire # pythonの関数等をコマンドラインから使えるようにする。
!pip install filterpy # カルマンフィルター

import sys
extend_pathes = [
    "/usr/local/lib/python3.10/dist-packages/filterpy-1.4.5-py3.10.egg",
    "/content/drive/MyDrive/Colab Notebooks/yolox/motpy",
]
for p in extend_pathes:
  if p not in sys.path:
    sys.path.append(p)
print("\n".join(sys.path))
```
>...
Successfully built fire
...
Successfully installed fire-0.5.0
...

インストールに成功しているとの出力が表示されればOKです。またこれらは、pythonのパスに追加する必要があるのでsys.pathに追加しています。

これからmotpyのサンプルを実行してみるわけなのですが、colaboratoryでopencvのcv2.imshowを実行させるためのパッチをあてるために、以下のようにexamples/detect_and_track_in_video.pyを編集します。

```python:examples/detect_and_track_in_video.py
# cv2.imshow('frame', frame) ← 137行目をコメントアウト
from google.colab.patches import cv2_imshow
cv2_imshow(frame)
```

137行目のcv2.imshowをgoogle.colab.patches.cv2_imshowに変更します。

```python:motpyのサンプルの実行
%run examples/detect_and_track_in_video.py --video_path=./assets/video.mp4 --detect_labels=['car','truck'] --tracker_min_iou=0.15 --show_detections=True --architecture="fasterrcnn"
```
>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/11234c99-17aa-7aa1-ea82-fe8a924801c0.png)

examples/detect_and_track_in_video.pyを実行すると、画像が何枚も出力されます。画像の中の太枠はトラッキングしているところ、細い枠は物体検出したところです。3枚目の画像からトラッキング結果が含まれるようになります。

## 3.2. 物体検知をyoloxで推論したjsonファイルに置き換えて、トラッキングの出力をjsonファイルに保存する

motpyの**examples/detect_and_track_in_video.py**を編集します。

```python:examples/detect_and_track_in_video/py
# 94行目付近のrunメソッドがFireによってコマンドラインから呼び出される。
def run(video_path: str, detect_labels,
    # ...

    ''' whileの前で動画をキャプチャするwhileの前でyoloxで出力したjsonファイルを読み込む '''
    import json
    with open("../stored_yolox_outputs.json") as f:
        stored_detections = json.load(f)
    counter = 0         # 動画のフレームカウント
    stored_outputs = [] # トラッキング結果の保存先
    ''' ↑ 追加はここまで '''

    while True:
        # ...

        # 123行目付近の元々の物体検知をコメントアウトする。
        # detections = detector.process_image(frame)

        ''' yoloxの出力を保存したjsonから検出結果を取り出す '''
        detections = list(map(lambda x:Detection(np.array(x)[:4]),stored_detections[counter]))

        # ...

        ''' トラッキング結果(active_tracks)をリストに保存する '''
        stored_outputs.append(list(map(lambda x:x.box.tolist()
            +[x.score]+[x.class_id]+[x.id],active_tracks)))

        # トラッキング結果を描画する部分は、jsonファイル作成のためには不要なので処理のスピードアップ
        # のためにコメントアウトする。

        # if show_detections:
        #     for det in detections:
        #         draw_detection(frame, det)

        # for track in active_tracks:
        #     draw_track(frame, track, thickness=2, ...

        # cv2.imshow('frame', frame)
        # c = cv2.waitKey(viz_wait_ms)
        # if c == ord('q'):
        #     break

    # whileの後でトラッキング結果をjsonファイルに保存する。
    stored_outputs_filename = "stored_tracked_outputs.json"
    with open(stored_outputs_filename,"wt") as f:
        json.dump(stored_outputs,f)
        print(f"saved to {stored_outputs_filename}")
```
examples/detect_and_track_in_video.pyは[Python Fire](https://github.com/google/python-fire)によってrunメソッドがコマンドラインから呼び出されるような感じになります。
動画をキャプチャするwhileの前で、yoloxで作成したjsonファイルを読み込みます。また、トラッキングの出力を保存するリスト、フレームカウントの変数を作成しておきます。
元々はdetector.process_imageという関数によってpytorchの物体検知ライブラリを呼び出す部分をコメントアウトし、代わりに読み込んだjsonファイルから物体検出結果を取り出します。
そしてmotpyのトラッキング結果(active_tracks変数)をリストに保存しておき、動画キャプチャのwhileが終わった後でファイルに出力します。

**motpyの出力フォーマット**
```python:motpyの出力フォーマット
Track = collections.namedtuple('Track', 'id box score class_id')
```
motpyが出力するトラッキングの結果は、[pythonの名前付きタプル](https://docs.python.org/ja/3.8/library/collections.html#collections.namedtuple)で出力されます。名前付きタプルは連想配列のようにx.idと名前付きで要素にアクセスできます。

# 4. トラッキング結果を表示する

今回はhtmlのvideoタグで動画を表示し、canvasで枠を書いたり拡大したりとしました。ここではそのときに面倒だったところをメモしておきます。

**面倒だったところ**
javascriptで動画のフレーム番号を正確にする方法がよく分からない。
- javascriptで動画解析をする際によく利用される方法としてvideoタグからcanvasにコピーして、getImageDataでピクセルデータを得るというのがある。しかし動画の再生タイミングとcanvasへのコピータイミングが一致しないので、例えば動画では2フレームぐらい進んだのcanvasへは1回しかコピーしていないといったずれが生じてくる。
- 今回のトラッキング結果は、フレームごとにトラッキングしている物体の情報が保存されている。よって動画の開始から何番目のフレームか、といった情報がずれるとトラッキング結果がずれてしまうことになる。
- javascriptの[VideoFrame](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame)のようなインターフェースがあるようだが、よく使い方が分からないので以下のようにして対処した。

**対処方法**
最初に全フレームを抽出する。
- videoタグの[playbackRate](https://developer.mozilla.org/ja/docs/Web/API/HTMLMediaElement/playbackRate)を使って動画をゆっくり再生する。ゆっくり再生する理由は、フレームの取りこぼしが無いように。
- HTMLの描画更新タイミング([requestAnimationFrame](https://developer.mozilla.org/ja/docs/Web/API/window/requestAnimationFrame)を用いて描画関数が呼ばれるタイミング)でvideoタグからcanvasにコピーする。
    - canvasから[getImageData](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData)でピクセルデータを取得する。
    - 最後に取得したピクセルデータと異なっていれば新しいフレームとして保存する。
- javascriptでvideo.play()したところから[endedイベント](https://developer.mozilla.org/ja/docs/Web/API/HTMLMediaElement/ended_event)の前までのフレームを取得する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/ddd80ce9-404d-cf1a-08c3-7026d52d88cc.png)

フレームの抽出後は、大きなcanvasでそのフレームを描画し、小さなcanvasを作って以下のようなコードで拡大表示する。

```javascript:指定部分の拡大表示
// トラッキングした物体の枠
const bbox = [xmin,ymin,width,height];
// framesは保存した全フレーム、frameCountは現在表示しているフレーム番号
const bmp = await createImageBitmap(frames[frameCount], ...bbox);
// ctxは描画するcanvasのcontext, a_whはそのcanvasの[幅,高さ], 色枠のためにpaddingを10pxとる
ctx.drawImage(bmp, 10, 10, ...a_wh.map(e => e - 20));
// 色枠の描画例
ctx.lineWidth = 20;
ctx.strokeStyle = "a6cee3";
ctx.strokeRect(0, 0, ...a_wh);
```

# 5. 制限

1. 個別に車両を認識しているわけではないので、バイクが**2周目**に来たときに1週目と同じ車両としてトラッキングすることができません。
    1. motpyはCNNの特徴マップを使うオプションもあると思うので、その検討が必要です。
    1. または（かつ？）yolox-xで個別識別できるようにカスタムデータをたくさん作って学習する。
1. カメラが移動する場合の検証ができていません。
    1. motpyのカルマンフィルターがもしかするとカメラ移動に対応しているかもしれませんが、検証は全くしていません。

# 付録 ファイルフォーマット

**yolox-xによる物体検知の結果**
```json:stored_yolox_outputs.json
[ "← json全体のカッコ"
    [ "← フレームごとのカッコ"
        [ "← 1物体ごとのカッコ"
            xmin, "← bbox の左上、右下のポイント"
            ymin,
            xmax,
            ymax,
            object score, "← このアンカーに物体がある確率"
            class score, "← クラス値の確率"
            class value, "← クラス値"
        ],
        [ "← 2つ目の物体"
        ],
        ... "← 3つ目以降"
    ],
    [ "← 2つ目のフレーム"
    ],
    ... "← 3つ目以降のフレーム"
]
```

**motpyによるトラッキングの結果**
```json:stored_tracked_outputs.json
[ "← json全体のカッコ"
    [ "← フレームごとのカッコ"
        [ "← 1物体ごとのカッコ"
            xmin, "← bbox の左上、右下のポイント"
            ymin,
            xmax,
            ymax,
            score, "← trackerの出力するスコア(たぶん使われていない？)"
            class value, "← クラス値"
            id, "← 物体の識別子"
        ],
        [ "← 2つ目の物体"
        ],
        ... "← 3つ目以降"
    ],
    [ "← 2つ目のフレーム"
    ],
    ... "← 3つ目以降のフレーム"
]
```


# 参考
yoloxの公式

https://github.com/Megvii-BaseDetection/YOLOX

motpyの公式

https://github.com/wmuron/motpy