*Interp移植録 - 単眼視深度推定 / SC-Depth (OnnxInterp)

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

数年前になるが、MiDaSと言う単眼視深度推定(Monoular depth estimation)モデルを Nerves/Raspberry-pi3で動かして遊んでみた。数ある単眼視深度推定モデルの中から MiDaSを選んだ理由は、ロバスト性に優れているという触れ込みだったからだ。なんでも、互換性の乏しい複数のデータセットに、ゴリゴリと工夫を凝らして学習しているそうだ。深度アノテーション付きのデータセットを作成するコストが高いため、在りもので賄おうということのようだ。どうやら深度推定では、学習に用いるデータセットを用意するところに一つ目の大きな課題があるらしい。

課題があればその解決を目指すリサーチャーがいて、"Self-Supervised Monocular Depth Prediction"なんてものが提案されている。今回は、そんなモデルの一つ SC-Depthで遊んでみようと思う。

# 1.Original Work
単眼視深度推定モデルを "Self-Supervised"に学習する方法は大きく二つあって、一つはステレオ視を、もう一つは単眼視エゴモーションを援用する方法のようだ。SC-Depthは後者の方法を採用している。そしてSC-Depthが特にフォーカスしているのは、(1)移動物体に対する深度推定の精度の向上と、(2)物体境界部で推定結果がぼやける課題(オクルージョンに起因する)の解決。彼らのアプローチで目を引く点は、別の学習済み単眼視深度推定モデル(LeReS)を持ってきて、その出力 -- pseudo-depthと呼んでいる -- を足場としているところかな(下図の一番上のフロー)。より詳しくは論文を参照のこと。

- SC-DepthV3: Robust Self-supervised Monocular Depth Estimation for Dynamic Scenes
https://arxiv.org/abs/2211.03660

- GitHub: SC_Depth
https://github.com/JiawangBian/sc_depth_pl

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/21d14301-d674-bb50-5428-a99d51238c0e.png">


# 2.準備
SC-Depthの学習済みパラメタ(Pytorchのcheckpoint)は、下の OneDriveに置かれている。これを OnnxInterpで利用できるように ONNXモデルに変換しよう。

https://onedrive.live.com/?authkey=%21AN9KaLjLL78kdKY&id=36712431A95E7A25%213266&cid=36712431A95E7A25

<img width="100%"
src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/d37433d1-a764-4967-34ee-93e838a0f57a.png">

Pytorchのcheckpointにはモデルのグラフは含まれていないので、本家プロジェクトのモデル定義のコードが必要になる。プロジェクトをまるっと `git clone`しよう。

```shell
git clone https://github.com/JiawangBian/sc_depth_pl
cd sc_depth_pl
pip install -r requirements.txt
```

残念ながら、本家プロジェクトには ONNXモデルをエクスポートするスクリプトは添付されていない。同梱の`inference.py`を参考に、ちょこちょこっと自前で用意した。

```python: export_onnx.py
import torch
import torch.onnx
from path import Path
import os

from config import get_opts, get_training_size

from SC_Depth import SC_Depth
from SC_DepthV2 import SC_DepthV2
from SC_DepthV3 import SC_DepthV3


@torch.no_grad()
def main():
    hparams = get_opts()

    if hparams.model_version == 'v1':
        system = SC_Depth(hparams)
    elif hparams.model_version == 'v2':
        system = SC_DepthV2(hparams)
    elif hparams.model_version == 'v3':
        system = SC_DepthV3(hparams)

    output_dir = Path(hparams.output_dir)
    output_dir.makedirs_p()

    system = system.load_from_checkpoint(hparams.ckpt_path, strict=False)

    model = system.depth_net
    model.eval()

    # training size
    training_size = get_training_size(hparams.dataset_name)
    dummy_input = torch.randn(1, 3, *training_size)

    # export the model
    torch.onnx.export(model,
        dummy_input,
        output_dir / Path(hparams.ckpt_path).stem + ".onnx",
        export_params=True,
        #opset_version=10,
        do_constant_folding=True,
        input_names=["input.0"],
        output_names=["output.0"],
        #dynamic_axes={}
        )


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

上の OneDriveからダウンロードした学習済みパラメタを ./ckptsディレクトリに置いたとすると、次のコマンド・ラインを実行することで ./onnx_modelディレクトリに変換されたONNXモデルが出来上がる。

```shell
!python export_onnx.py --config configs/v3/ddad.txt --output_dir onnx_model --ckpt_path ckpts/ddad_scv3/epoch\=99-val_loss\=0.1438.ckpt
```

このままのONNXモデルでも利用できるのだが、[ONNX Simplifier](https://github.com/daquexian/onnx-simplifier)でシェイプアップしておこう。

```shell
onnxsim "epoch=99-val_loss=0.1438.onnx" "sc_depth-epoch=99-val_loss=0.1438.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.2"},
  {:kino, "~> 0.8.0"}
])
```

SC-Depthモデルの出力は、入力画像と同じ縦横サイズを持つ深度マップ。各行・列の要素の値は、入力画像中の対応する位置にある物体までの深度を表している。その値が大きいほど物体までの距離が遠いようだ。ただ、距離の単位やリニアリティについてはよく分からない。単なる相対値なのかなぁ? まぁなにはともあれ、この深度マップを可視化したいので、要素の値のmin-maxで正規化してグレイ画像(0..255)に変換する。さらに格好良く見せる常套手段は、ヒートマップへの変換だが、この処理はデモ・モジュールに先送る。

> [モデル・カード]
> - inputs:
> [0] f32:{1,3,384,640} - RGB画像,NCHWレイアウト,画素は平均/分散: R{114.75,57.375},G{114.75,57.375},B{114.75,57.375}で正規化
> - outputs:
> [0] f32:{1,1,384,640} - 入力と同じサイズの深度マップ。各要素の値は、その位置にある物体までの深度を表す。値が大きいほど距離が遠い(相対値?)。

```elixir
defmodule ScDepth do
  @width  640
  @height 384

  alias OnnxInterp, as: NNInterp
  use NNInterp,
    model: "model/sc_depth-epoch=99-val_loss=0.1438.onnx",
    url: "https://github.com/shoz-f/onnx_interp/releases/download/models/sc_depth-epoch.99-val_loss.0.1438.onnx",
    inputs: [f32: {1,3,@height,@width}],
    outputs: [f32: {1,1,@height,@width}]

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

    # prediction
    output0 = session()
      |> NNInterp.set_input_tensor(0, input0)
      |> NNInterp.invoke()
      |> NNInterp.get_output_tensor(0)

    # postprocess
    {w, h, _, _} = CImg.shape(img)

    output0
    |> CImg.from_binary(@width, @height, 1, 1, range: min_max(output0), dtype: "<f4") # Gray image
    |> CImg.resize({w,h})
  end
  
  defp min_max(bin) do
    t = Nx.from_binary(bin, :f32)
    {
      Nx.reduce_min(t) |> Nx.to_number(),
      Nx.reduce_max(t) |> Nx.to_number()
    }
  end
end
```

デモ・モジュール LiveScDepthは、ScDepthから受け取った深度マップ(グレイ画像)を色付け描画する。

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

    depth = ScDepth.apply(img)
      |> CImg.color_mapping(:jet)

    Kino.Layout.grid(
      Enum.map([img, depth], &CImg.display_kino(&1, :jpeg)),
      columns: 2)
  end
end
```

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

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

画像を与え、SC-Depthを実行する。

```elixir
LiveScDepth.run("sample.jpg")
```

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14158/b90db8bc-b78c-2cfc-f3ef-9c0a374c7a37.png">

# 5.Epilogue
単眼視深度推定の SC-Depthを移植して遊んでみた。

*Interpシリーズは inferenceエンジンなので、残念ながらリサーチャーたちが SC-Depthの学習ステップに盛り込んだ工夫を体感することはできない。リソースが制限されたエッジ・デバイスで、学習なりチューニングなりを行うのはとてもハードルが高いと思うのだが、ちらほらと "On-device Training"というキーワードを見かけたりするので、少しこの方面も調べてみようかなと思う。

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