スクープ!! Axonで読める YOLOv4モデルが発見される

# 0.Prologue
「下手な鉄砲も数撃ちゃ…」とはよく言ったものだ。それはまさかの出来事であった。

2022年11月某日、拙作 OnnxInterpの発展的開発終了を記念して、初代TflInterpの開発の発端となった Y系物体検出モデルを一堂に集めて[ネタのプロジェクト](https://qiita.com/ShozF/items/21bcf9de0791f8d7d2da)「怒涛のよろず祭り」を作成していた。そんな最中、これらのモデルの中で Axonで読めるモノがあるのかなぁと悪魔の囁きが聞こえ、無駄骨とは思いつつも手を止めて AxonOnnx.importで片っ端から試し始めた。そう、ご存じの通り AxonOnnx.importは堅物で、ここがダメ、あそこがダメと言ってなかなかネットに流布しているonnxモデルを受け付けてくれないのだ。

そしてその瞬間がやって来た…
「ん?? なんか今エラーしなかったような? いやそんな筈はない。見間違いだ」 
と、3～4回同じことを繰り返し…
「ええぇ!!! この YOLOv4.onnxのモデル、Axonにコンバートできるやん!!!!!!!!!!」
と我が目を疑ったのである。

という訳で、Axonで読める YOLOv4のモデルが見つかったよぉぉぉ:confetti_ball:

# 1.何はともあれ動かしてみる
堅物の AxonOnnx.importの関門を通るのは分かったが、果たしてちゃんと動作するのか? 小生は疑り深い性格である:unamused:

幸い、「祭り」プロジェクトで動作確認が出来ている YOLOv4.exモジュールとデモ・ドライバYOLOs.livemd[*1]がある。これを AxonInterp向けにチョコチョコっと修正して Livebookでテストしてみよう[*2]。notebookの名前は `YOLOv4.livemd`でいいかな。

[*1]mix用もあるのだが、やっぱりビジュアルな方が分かり易いよね:wink:
[*2]AxonInterpを含む *Interpファミリーは、お互いに実装コードを流用し合えることを目指しています。

https://github.com/shoz-f/axon_interp/blob/main/demo_yolov4/YOLOv4.livemd

```elixir: YOLOv4.ex
defmodule YOLOv4 do
  @moduledoc """
  Original work:
    Pytorch-YOLOv4 - https://github.com/Tianxiaomo/pytorch-YOLOv4
  """

  use AxonInterp,
    model: "./model/yolov4_1_3_608_608_static.onnx",
    url: "https://github.com/shoz-f/axon_interp/releases/download/0.0.1/yolov4_1_3_608_608_static.onnx",
    inputs: [f32: {1, 3, 608, 608}],
    outputs: [f32: {1, 22743, 1, 4}, f32: {1, 22743, 80}]

  @prepro CImg.builder()
          |> CImg.resize({608,608})
          |> CImg.to_binary([{:range, {0.0, 1.0}}, :nchw])

  def apply(img) do
    # preprocess
    input0 = CImg.run(@prepro, img)
 
    # prediction
    outputs = session()
      |> AxonInterp.set_input_tensor(0, input0, [:binary])
      |> AxonInterp.invoke()

    # postprocess
    boxes = AxonInterp.get_output_tensor(outputs, 0, [:binary])
    scores = AxonInterp.get_output_tensor(outputs, 1, [:binary])

    PostDNN.non_max_suppression_multi_class(
      {22743, 80}, boxes, scores,
      boxrepr: :corner,
      label: "./model/coco.label"
    )
  end
end
```

```elixir: LIveYOLOv4.ex
defmodule LiveYOLOv4 do
  @palette CImg.Util.rand_palette("./model/coco.label")

  def run(path) do
    img = CImg.load(path)

    with {:ok, res} <- YOLOv4.apply(img) do
      IO.inspect(res)

      Enum.reduce(res, CImg.builder(img), &draw_item(&1, &2))
      |> CImg.display_kino(:jpeg)
    end
  end

  defp draw_item({name, boxes}, canvas) do
    color = @palette[name]

    Enum.reduce(boxes, canvas, fn [_score, x1, y1, x2, y2, _index], canvas ->
      [x1, y1, x2, y2] = PostDNN.clamp([x1, y1, x2, y2], {0.0, 1.0})

      CImg.fill_rect(canvas, x1, y1, x2, y2, color, 0.35)
    end)
  end
end
```

そうそう、Axonは EXLAを援用しないと死ぬほど遅いので、setupセルの Mix.installに configオプションを付けておこう:wink:

```elixir
File.cd!(__DIR__)
# for windows JP
# System.shell("chcp 65001")

Mix.install(
  [
    {:axon_interp, "~> 0.1.0"},
    {:cimg, "~> 0.1.14"},
    {:postdnn, "~> 0.1.5"},
    {:kino, "~> 0.7.0"}
  ],
  config: [
    nx: [default_defn_options: [compiler: EXLA]]
  ]
  # system_env: [{"NNINTERP", "Axon"}]
)
```

話は前後するが、YOLOv4.livemdを実行するディレクトリの構造は下記の様にした。今回発見した YOLOv4の onnxモデル`yolov4_1_3_608_608_static.onnx`と`coco.label`はサブディレクトリmodelに置く[*3]。それと、テスト用の画像はいつもの`dog.jpg`でいいよね。

[*3]尤も、上の`YOLOv4.ex`では、所定のモデル・ファイルが見つからなければ指定のURLからダウンロードせよとAxonInterpに指示していたりする:yum:

```text
├── YOLOv4.livemd
├── dog.jpg
└── model
    ├── coco.label
    └── yolov4_1_3_608_608_static.onnx
```

さぁ準備が整った。テストだ、テスト。わくわくする:blush:
モジュール YOLOv4の実体は GenServerなので一度だけ start_link/1を実行。
```shell
YOLOv4.start_link([])
```
そいでもってラン♬
```
LiveYOLOv4.run("dog.jpg")
```
おおおおぉ!
ちゃんと動くではないか。
ここにきて、やっと納得した小生であった:stuck_out_tongue_winking_eye:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/42b7d36b-1bb6-d0cc-bca0-d5d89a180aae.png)

PS. 何故か Axonに変換した YOLOv4モデルはシリアライズ、逆コンバートが出来ないようで。一方通行?
どうなってんだこれぇ?

# 2.オリジナル・ワーク
さて、無事に Axonで YOLOv4が動いた訳ですが、いったいこの pre-traindモデルを何処から拾ってきたモノのか気になりますよね。本家本元の Darknetプロジェクト……ではなく、それの Pytorchへの移植を行っている下記のプロジェクトから拾ってきたのでした。先人達のお仕事に感謝・感謝です:two_hearts:

https://github.com/Tianxiaomo/pytorch-YOLOv4

でもって、onnxモデルの生成は、プロジェクトに記述されていた手順を元に google Colabotryで下記の通り行いました。出来上がった onnxモデルはオリジナルの Darknet版をコンバートしたモノの様です。利便性を考えて、この onnxモデルを小生の GitHub ならびに Google Driveで公開中です。

> GitHub:
> -- https://github.com/shoz-f/axon_interp/releases/download/0.0.1/yolov4_1_3_608_608_static.onnx
> Google Drive:
> -- https://drive.google.com/file/d/1oY9Pv4Q_MfPolG4sRhydf1GGFnv7556c/view?usp=share_link

```: setup
!git clone https://github.com/Tianxiaomo/pytorch-YOLOv4
%cd pytorch-YOLOv4
!pip install onnx onnxruntime
```

```: download darknet weights
!wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights
```
```: convert to onnx
!python demo_darknet2onnx.py cfg/yolov4.cfg data/coco.names yolov4.weights data/dog.jpg 1
```

モデルの入力は一つ、出力は二つで、それぞれ次の仕様となっています。特筆すべき点は、このモデルの出力はそのまま後処理の Multi-class NMSに渡せるという点です(YOLOv4.ex参照)。これは、DNN推論プログラムの実装者にとってとても有り難いことです:blush:

> 入力1:
> RGB画像を{608,608}にリサイズし、各画素を{0.0,1.0}の範囲のfloat32に正規化、NCHW変換した tensor
>
> 出力1:
> float32[1][22743][1][4]の tensorで、22743個のバウンディング・ボックスの対角点座標(x1,y1)-(x2,y2)を{608,608}に対する比率の表したもの。
>
> 出力2:
> float32[1][22743][80]の tensorで、22743個の各バウンディング・ボックスが cocoの80カテゴリの各々オブジェクトで有り得る確率を表したもの。

余談ですが、YOLOv4が発表された頃には、Object Detectionモデルはシステマティックなアーキテクチャが主流となっており、それまでに蓄積された知見や技術を組み合わせ、より高精度かつ高速性を追い求める時代となっていたようです。YOLOv4も多分に漏れず、下記の主流のアーキテクチャで、特徴マップを作るBackboneに"CSPDarknet53"を、オブジェクトのクラスとバウンディング・ボックスを推定するHeadに"YOLOv3"を、そしてBackbornの各層(解像度?)の情報を混合してHeadに渡すNeckに"SPP"/"PAN"を配しているとのことです。それ以外にも、ユニット/オペレーションレベルでの工夫もされているようです(詳しくは理解していない:sweat_smile:)。

以上、聞き齧りでした:speech_balloon:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/dcc8e675-a9ee-2c8e-6ca1-ea2264584348.png)


# 3.Epilogue
さて、ここまで駄文にお付き合い戴きありがとう御座いました。
見ての通り技術的な要素が一つもないネタの記事でした。悪しからず:stuck_out_tongue_closed_eyes:

今年2022年を振り返ると、C++&Elixirで細々と俺っちDNN推論モジュール*Interpを作り、ゴミをせっせと生産していました。来年はもう少し役立つモノに取り組みたいですね。

少し早いですが、メリークリスマス。
それではまた

# 参考文献
- [YOLOv4: Optimal Speed and Accuracy of Object Detection](https://arxiv.org/pdf/2004.10934)