*Interp移植録 - Driving Perception / YOLOP (OnnxInterp)

# 0.Prologue
暇つぶしに、興味を引いた DNNアプリを *Interpに移植して遊んでいる。
本稿はその雑記&記録。

自動車の自動運転に関連する DNNモデルもいろいろと提案されている。自動運転に必要な機能には "前方にいる車の検出"、"車両通行帯の検出"、"区画線(白線)の検出"などがあるが、それら三つを同時に検出してしまう YOLOPという DNNモデルがある。今回は、これを移植してみよう。

# 1.Original Work
先に触れた通り、YOLOPは三つの対象を同時に検出出来るところが斬新でかつセールスポイントとのことだ。モデルの構造は下図の通りで、バックボーン+ネック+ヘッドと物体検出では至って平均的な構成だと思う。三つの異なる対象が検出できる秘密は、きっとトレーニングにあると思うのだが…ちっとは勉強しなきゃなぁ:sweat_smile:

> 2023.01.11追記
バックボーンは YOLOv4の CSP-Darknet、ネックは SSP+FPN。損失関数は三つのタスクの損失関数の線形和で、学習則は、(a)end-to-endに３タスク同時、(b)step-by-stepに最初に"車"の検出の学習、次に"車両通行帯","区画線"の検出の学習、最後に３タスク同時の学習、の２パターンを評価したようだ。
>
> 一見すると、"車"と"車両通行帯","区画線"の検出は全く異なるタスクの様に見えるのだが、損失関数を線形和で繋いでかつ同時に学習しても誤差が振動せずに収束するらしい。ちょっぴり驚いた。まぁ極言すれば、どのタスクも画像からある特徴を持つ領域を切り出すセマンティック・セグメンテーションだからと言えなくはないが…



- YOLOP: You Only Look Once for Panoptic Driving Perception
https://arxiv.org/abs/2108.11250

- GitHub: You Only 👀 Once for Panoptic ​ 🚗 Perception
https://github.com/hustvl/YOLOP

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/c60513ad-02c7-6bf7-0226-8f9ce27fcedf.png">


# 2.準備
YOLOPの ONNXモデルは、次のGitHubから調達する。

https://github.com/hustvl/YOLOP/raw/main/weights/yolop-640-640.onnx

# 3.OnnxInterp用のLivebookノート

Mix.installの依存リストに記述するモジュールは下記の通り。

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

Mix.install([
  {:onnx_interp, path: ".."},
  {:cimg, "~> 0.1.18"},
  {:nx, "~> 0.4.1"},
  {:kino, "~> 0.8.0"}
])
```

前処理の入力画像のリサイズは aspect比保存で行うべきなのだが、後処理で煩雑な逆変換が必要となるので手抜きする。元の画像が極端に長細くなければ、それなりの結果が得られるだろう。

モデルの出力は、画像内にいる車のBBoxと車両通行帯、区画線の領域を表す640x640のマップ。BBoxはいつもの様にNMSでふるい掛けする。また、マップには各点が [背景である確率,対象である確率]のペアが格納されているので、背景よりも対象の確率が高い点を対象に含まれる点と見なしてマスク画像(8bit/0-1画像)を生成する。

最後に、推論結果は入力画像にスケーリングしてから返すことにする。

> [モデル・カード]
> - inputs:
> [0] f32:{1,3,640,640} - RGB画像,NCHWレイアウト,画素は平均/分散:R{103.53, 57.375},G{116.28, 57.12},B{123.675, 58.395}で正規化
> - outputs:
> [0] f32:{1,25200,6} - 対象物のBBox[,,0:4]とその評価値(クラス評価値[,,4],対象物評価値[,,5])、BBoxの座標値/サイズは 640x640のグローバル座標系で表記<br>
> [1] f32:{1,2,640,640} - 車両通行帯の評価値マップ(背景[,0,,],車両通行帯[,1,,])
> [2] f32:{1,2,640,640} - 区画線の評価値マップ(背景[,0,,],区画線[,1,,])

```elixir
defmodule YOLOP do
  @width  640
  @height 640

  alias OnnxInterp, as: NNInterp
  use OnnxInterp,
    model: "model/yolop-640-640.onnx",
    url: "https://github.com/hustvl/YOLOP/raw/main/weights/yolop-640-640.onnx",
    inputs: [f32: {1,3,@height,@width}],
    outputs: [f32: {1,25200,6}, f32: {1,2,@height,@width}, f32: {1,2,@height,@width}]

  def apply(img) do
    # preprocess
    input0 = img
      |> CImg.resize({@width, @height})
      |> CImg.to_binary([{:gauss, {{103.53,57.375},{116.28,57.12},{123.675,58.395}}}, :nchw])

    # prediction
    outputs = session()
      |> OnnxInterp.set_input_tensor(0, input0)
      |> OnnxInterp.invoke()

    det_out        = OnnxInterp.get_output_tensor(outputs, 0) |> Nx.from_binary(:f32) |> Nx.reshape({:auto, 6})
    drive_area_seg = OnnxInterp.get_output_tensor(outputs, 1) |> Nx.from_binary(:f32) |> Nx.reshape({2, :auto})
    lane_line_seg  = OnnxInterp.get_output_tensor(outputs, 2) |> Nx.from_binary(:f32) |> Nx.reshape({2, :auto})

    # postprocess
    img_size = CImg.shape(img) |> then(fn {w,h,_,_} -> {w,h} end)
    {
      :ok,
      decode_bbox(det_out, img_size),
      decode_segments(drive_area_seg, img_size),
      decode_segments(lane_line_seg, img_size)
    }
  end

  defp decode_bbox(t, {w,h}) do
    boxes  = Nx.slice_along_axis(t, 0, 4, axis: 1)
    scores = Nx.multiply(Nx.slice_along_axis(t, 4, 1, axis: 1), (Nx.slice_along_axis(t, 5, 1, axis: 1)))

    {:ok, res} = NNInterp.non_max_suppression_multi_class(__MODULE__,
      Nx.shape(scores), Nx.to_binary(boxes), Nx.to_binary(scores),
      iou_threshold: 0.45, score_threshold: 0.25
    )

    # fit to input image
    scale_x = fn x -> round(x * w / @width) end
    scale_y = fn y -> round(y * h / @height) end
    Enum.map(res["0"], fn [score, x1, y1, x2, y2, index] ->
      [score, scale_x.(x1), scale_y.(y1), scale_x.(x2), scale_y.(y2), index]
    end)
  end

  defp decode_segments(t, img_size) do
    Nx.greater(t[1], t[0])
    # convert to mask image
    |> Nx.to_binary() |> CImg.from_binary(640, 640, 1, 1, dtype: "<u1")
    # fit to input image
    |> CImg.resize(img_size)
  end
end
```

デモ・モジュール DemoYOLOPは、YOLOPから受け取ったBBoxと車両通行帯,区画線のマスク画像を入力画像上に描画する。

```elixir
defmodule DemoYOLOP do
  def run(path) do
    img = CImg.load(path)

    with {:ok, cars, drive_area, lane_line} = YOLOP.apply(img) do
      CImg.builder(img)
      |> CImg.paint_mask(drive_area, {255,255,0})
      |> CImg.paint_mask(lane_line,  {255,0,0})
      |> draw_item(cars, {0,192,0})
      |> CImg.display_kino(:jpeg)
    end
  end

  defp draw_item(canvas, bbox, color) do
    Enum.reduce(bbox, canvas, fn [_score, x1, y1, x2, y2, _index], canvas ->
      CImg.fill_rect(canvas, x1, y1, x2, y2, color, 0.3)
    end)
  end
end
```

# 4.デモンストレーション
`YOLOP`を起動する。

```elixir
YOLOP.start_link([])
```

画像を与え、YOLOPを実行する。

```elixir
DemoYOLOP.run("01.jpg")
```

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/a3ebfd67-9554-dd66-65b2-4954c2b5addc.png">

# 5.Epilogue
自動運転ナビ系の YOLOPを移植してみた。
AIによる自動運転を実社会に実装するには、AIの推論結果をどの様にして運転機構に繋ぐのか、安全の担保はどうするのか、万が一事故が起こった場合の法的責任はどうするのかなどまだまだ課題は山積みだと思う。しかしながら、そんな未来がすぐそこまで来ている様な気もする。まあ、小生が子供の頃に夢見た「空飛ぶ車が行き交う未来」とは少し違った未来ではあるが。さて、いったいどんな世界になるのかな?

# Appendix
- OnnxInterpのノート
https://github.com/shoz-f/onnx_interp/blob/main/demo_yolop/YOLOP.livemd