試してみる Monocular Depth Estimation: MiDaS on Livebook

## 1.Prologue

職業柄、最近 Deep Learningを応用した単眼視深度推定(Monocular Depth Estimation)がとても気になっている。カメラのみを用いて深度(Ｚ距離)測定する方法といえば、2台(二眼視)またはそれ以上(多眼視)のカメラを用いて三角測量を行うステレオ・ビジョンがポピュラーなのだが、単眼視とはなんともユニークで話ある。その仕組みと、どの程度現場で使えるモノなのかの感触を掴みたいと思っている。

ところで、先日Qiitaのある記事がきっかけで、拙作 **cimg_ex**のデモを Livebook上に書いて動かしてみる機会があった。やってみると、これがまぁ Jupyter上での作文作業と同じく、Elixirコードの動作をリアルタイムに確認しつつその説明文を起こすことができ、とてもお手軽かつストレス・フリーな体験であった。

そんな訳で、Livebook上でちょこっと「単眼視深度推定」をデモってみようかと思い立ったのである。道具箱には、かれこれ２年ほど前から取り組んでいる TensorFlow liteの Elixir拡張モジュール **tfl_interp**とその取り巻き ― **cimg_ex**など ― が入っている。学習済みの tfliteモデルさえ入手できれば、Python不要でサクッとデモることができる筈だ。<br>

さて、肝心の学習済み tfliteモデルの入手だが、昨今の情勢ではホットな Deep Learningモデルは Pytorchで記述されてることが多く、その場合 Pytorchモデルを "Pytorch → ONNX → TensorFlow → TensorFlow lite"とコンバートして tfliteモデルを得ることになる[* 1]。面倒な作業である…のだが、幸い ***MiDaS***と言うモデルが tflite版も提供していたので、これを使わせて貰うことにした。

> ***MiDaS***:
> [Towards Robust Monocular Depth Estimation: Mixing Datasets for Zero-shot Cross-dataset Transfer](https://github.com/isl-org/MiDaS)

***MiDaS***モデルの内部を少し覗いてみると、*RefineNet*が用いられており、どうやら Semantic Segmentationの技術を応用しているようだ。Deep Learningの技術は日進月歩で進化しているので、キャッチアップするのも大変である。<br>

> [RefineNet (Multi-Path Refinement Network):ディープラーニングによるSemantic Segmentation手法](https://blog.negativemind.com/2019/03/26/semantic-segmentation-by-multi-path-refinement-network/)


[* 1]余談になるが、[YOLOX](https://github.com/Megvii-BaseDetection/YOLOX)には tfliteモデルが用意されていなかった。そのため、まさしくこのコンバートの連鎖を辿ることとなった。そして厄介なことに、コンバートが成功する確約も、得られた tfliteモデルが正しく動作する確約も無いのだ。

### <u>Requirement</u>:

デモに用いた環境ほかは以下の通り。

* Windows WSL2/Ubuntu 20.04.2
* Elixir 1.13.1 (compiled with Erlang/OTP 24)
* Erlang/OTP 24 [erts-12.1.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
* CMake version 3.18 or later [* 2]
* unzip 6.0-25ubuntu1
* TensorFlow lite v2.7.0 (https://github.com/tensorflow/tensorflow.git)

[* 2]CMakeのインストール手順については、本文末の「補足２」を参照願いたい。

作業は WSL2/Ubuntuのホーム・ディレクトリ(~)下に "demo"ディレクトリを用意し、その中で行うこととした。

```shell
~$ mkdir demo
~$ cd demo
```

主要なファイルは次の様に配置される予定。

```
 ~/demo
├── livebook
│   └── model_opt.tflite - MiDaSモデルファイル
└── tfl_interp
    ├── lib   - Elixirソースコード
    ├── src   - tfl_interpの C++ソースコード
    ├── priv
    │   └── tfl_interp - TensorFlow liteの Elixir拡張モジュール(実行形式)
    ├── CMakeLists.txt - tfl_interpを cmakeでビルドする為の CMakeLists
    └── _build
         └── dev
             └── .cmake_build - tfl_interpや TensorFlow liteをビルドするための作業ディレクトリ
```

## 2.Install 'Livebook'

ローカルPCへのLivebookのインストールは下の通り (インストール先は ~/demo/livebook)
ここでは、Livebookが依存する Elixirモジュールの入手までを行っておく: `mix deps.get -- only prod`

<!-- livebook:{"break_markdown":true} -->

```shell
~/demo$ git clone https://github.com/elixir-nx/livebook.git
~/demo$ cd livebook
~/demo/livebook$ mix deps.get --only prod

~/demo/livebook$ cd ..

```

## 3.Install 'tfl_interp' and download "MiDaS" tflite model

拙作の **tfl_interp**は、一般の Elixirモジュールと少々取り扱いが異なる所がある。

一般の Elixirモジュールの場合は、メイン・アプリの "mix.exs"の依存モジュール・リストに登録し、おもむろに `mix deps.get` `mix compile`を実行すれば、自動的に当該モジュールのコンパイルも行われるのだが、**tfl_interp**では意図的に自動コンパイルが行われない様に取り扱うこととしている。

その理由は、**tfl_interp**の backendである TensorFlow liteのビルドと、それに先立つ TensorFlowファイルセットのダウンロードに、数十分オーダーの時間を要するからだ。つまり、一般の Elixirモジュールと同じように取り扱うと、**tfl_interp**を利用した新たな Deep Leraningアプリ・プロジェクトを開始する度に、もれなく数十分のコーヒーブレイクが付いてくることになるのだ…冗談じゃない。間抜け過ぎるだろ。

そんな訳で、**tfl_interp**はローカルPC上で単独のプロジェクトとして一度だけビルドし、目的アプリからはそのビルド成果物を "mix deps"の Pathオプション(:path)で借用することにした。

---

具体的なインストール手順は下の通り (インストール先は ~/demo/tfl_interp)

livebookと同じディレクトリ階層に **tfl_interp**のファイルセットをダウンロードし[* 3]、`mix cmake --config`でビルド&インストールを行う。先にも触れた通り、インストールが完了するまでに数十分の時間を要する。紅茶とお菓子と雑誌を用意してから取り掛かろう。Pathオプションについては後述。

> **2022/2/2追記**
> tfl_interp v0.1.3 でビルド・スクリプトに少し変更が入りました。
> 下記の手順の最後の行"mix cmake --config"を実行する前に、"~/demo/tfl_interp/mix.exs"ファイルの project/0の中にある次の行を削除またはコメント・アウトして下さい。

> 　　  compilers: [:cmake] ++ Mix.compilers(),

```shell
~/demo$ wget --no-check-certificate https://github.com/shoz-f/tfl_interp/archive/main.tar.gz
~/demo$ mkdir tfl_interp && tar xzvf main.tar.gz -C tfl_interp --strip-components 1
~/demo$ rm main.tar.gz
~/demo$ cd tfl_interp
~/demo/tfl_interp$ mix deps.get
~/demo/tfl_interp$ mix cmake --config
```


tfl_interpのビルド&インストールが成功すれば、tfl_interp/privに実行形式ファイルが置かれている筈なのでチェックしよう。OKのようだ、次へ進もう。:wink:

```shell
~/demo/tfl_interp$ ls -l priv
合計 5444
-rwxr-xr-x 1 shozo shozo 5573816  1月 17 01:52 tfl_interp

~/demo/tfl_interp$ cd ..
```
[* 3]`git clone https://github.com/shoz-f/tfl_interp`でも差し支えない。URLは ***hex***にも登録している。

<!-- livebook:{"break_markdown":true} -->

---

併せて ***MiDaS***の学習済みモデルをダウンロードする (インストール先は ~/demo/livebook)

```shell
~/demo$ wget -P livebook https://github.com/isl-org/MiDaS/releases/download/v2_1/model_opt.tflite
```

## 4.Run a demo of Midas inference

さて、準備が整ったので Livebookを起動する: `MIX=prod mix phx.server`<br>
ターミナルに表示された URL - この例では"http://localhost:8080/?token=aergubyrpc6hak7hq6dqtryhwukosq4p" - をブラウザで開き、下の画面が表示されれば成功だ。

```shell
~/demo$ cd livebook
~/demo/livebook$ MIX_ENV=prod mix phx.server
[Livebook] Application running at http://localhost:8080/?token=aergubyrpc6hak7hq6dqtryhwukosq4p
```

<img src="https://user-images.githubusercontent.com/1754093/149229813-b82788bf-4572-4857-81c3-a38d46bd906f.jpg" width="90%" />

さて、まっさらな状態から Livebookの notebookを書くならば、ここで画面右上の[New notebook]ボタンを押して始めればよい。以下、記事の要所々々に掲載したコードを notebookに入力し実行することになる。

尤も、この記事の中のコードは一度 Livebook上で試しており、コードにこの記事の文章を書き加えた notebookを Gistに保存している。お手軽にそれをインポートして試すこともできる。この場合は、画面右上の[Import]ボタンを押そう。

> notebookの保存場所: https://gist.github.com/shoz-f/02ba7f08e260c0d8688c82fabe7adc81

<img src="https://user-images.githubusercontent.com/1754093/149226265-32de41ad-5b73-4729-b7bb-24a22bf82daa.jpg" width="90%" />

---

ここから先がデモの Elixirコードだ。まず最初に、依存モジュールを `Mix.install`でインストールする。リストの書き方は、`mix deps`のそれと同じだ。先述した通り、**tfl_interp**に関しては ***hex***リポジトリではなく、ローカルPC上にインストールした **tfl_interp**のパスをPathオプションで指定する。

コード・セルの左上の実行ボタンを押そう。

```elixir
Mix.install([
  {:mix_cmake, "~> 0.1"},
  {:poison, "~> 3.1"},
  {:cimg, "== 0.1.6"},
  {:nx, "~> 0.1.0"},
  {:kino, "~> 0.3.1"},
  {:download, "~> 0.0.4"},
  {:tfl_interp, path: "../tfl_interp"}
])
```
どうでも良いことなのだが、Pathオプションで指定したモジュールは、いつものように depsディレクトリ下にコピーが取られることはなく、直接_buildディレクトリ下にバイトコード他が生成されるようだ。

---

***MiDaS***による深度推定のコードは下の通り。

**tfl_interp**では、tfliteモデル毎にモジュールを定義し、`use`文で TflInterpモジュールが用意する裏方機能を取り込むように書く。このモジュールが Deep Learningの推論モジュールとなる。モデル・ファイル(*.tflite)は、`use`のオプション・パラメタ`model:`にそのパスをセットして裏方に渡す。

今回のコードでは、CImg画像を受け取り深度推定結果を返す関数`apply/1`一つだけを定義している。大概の Deep Learning推論ルーチンは、`apply/1`と同様に (a)前処理、(b)推論実行、(c)後処理の３つの処理で出来ている。それぞれの処理を詳しく見る前に、***MiDaS***モデルの入出力仕様を確認しておこう。

> ***MiDaS***モデルの入出力仕様
> 
> * 入力tensor[0]:  
>   RGB画像 W:256pix×H:256pixの各色プレーンの輝度値を{float-32bit, -2.0～2.0}に正規化し、NHWC軸並びでシリアライズしたバイナリ列を入力テンソルに与える。
> * 出力tensor[0]:  
>   入力画像の各画素位置の相対深度推定値を格納した float-32bit 256x256配列を、NHWC軸並びでシリアライズしたバイナリ列。(※注)入力画像に依って、相対深度推定値が取りうる値の範囲は変化する。

それでは、コードを見ていこう。

#### (a)前処理 - コード中 "# preprocess"から始まるブロック

CImg.resize/2で入力画像`img`を W:256pix×H:256pixの画像にリサイズした後に、CImg.to_flat/2で各色プレーンの輝度値を {-2.0～2.0}の範囲の float-32bitに正規化&シリアライズしたバイナリ`bin`(numpy形式)を作成する。

#### (b)推論実行 - コード中 "# prediction"から始まるブロック

TflInterp.set_input_tensor/3でバイナリ`bin`を ***MiDaS***モデルの入力 tensor[0]にセットし、TflInterp.invoke/1で推論を実行する。その後、TflInterp.get_ouput_tensor/2で推論結果の出力tensor[0]をバイナリとして取り出し、float-32bit 256x256の Nx.Tensor `outputs`に変換する。

#### (c)後処理 - コード中 "# postprocess"から始まるブロック

***MiDaS***モデルの入出力仕様で見た通り、Nx.Tensor `outputs`に格納されている相対深度推定値は、その取りうる値の範囲が入力画像に依存して変動してしまう。推論結果を利用するアプリケーションにしてみればなんとも迷惑な話だ。そこで、相対深度推定値の最小値`min`、最大値`max`を用いて正規化を行い、CImg.create_from_bin/6で 8bit W:256pix×H:256pixのモノクロ画像に変換する。

```elixir
defmodule Midas do
  use TflInterp, model: "model_opt.tflite"

  @midas_shape {256, 256}

  def apply(img) do
    # preprocess
    bin =
      img
      |> CImg.resize(@midas_shape)
      |> CImg.to_flat(range: {-2.0, 2.0})

    # prediction
    outputs =
      __MODULE__
      |> TflInterp.set_input_tensor(0, bin.data)
      |> TflInterp.invoke()
      |> TflInterp.get_output_tensor(0)
      |> Nx.from_binary({:f, 32})
      |> Nx.reshape({256, 256})

    # postprocess
    [min, max] =
      [Nx.window_min(outputs, {256, 256}), Nx.window_max(outputs, {256, 256})]
      |> Enum.map(&Nx.squeeze/1)
      |> Enum.map(&Nx.to_number/1)

    _result =
      outputs
      |> Nx.subtract(min)
      |> Nx.divide(max - min)
      |> Nx.to_binary()
      |> CImg.create_from_bin(256, 256, 1, 1, "<f4")
  end
end
```

上記の `Midas`モジュールを利用するには、裏方にいる GenServerを起動し、**tfl_interp**の実体である Port拡張モジュールを spawnする必要がある。

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

以上で全てだ。

---

では早速 ***MiDaS***深度推定を試してみよう。入力画像は、Woods Hole Oceanographic Institutionのサイトから愛くるしいペンギンの写真をお借りした。

>Woods Hole Oceanographic Institution: https://www.whoi.edu
>
>Woods Hole Oceanographic Institution is the world's leading, independent non-profit organization dedicated to ocean research, exploration, and education. Our scientists and engineers push the boundaries of knowledge about the ocean to reveal its impacts on our planet and our lives.

```elixir
File.rm("graphics-SIPEX_P_Kimball-IMG_9445-1920x1280-1-2-1200x679.jpg")

img =
  Download.from(
    "https://www.whoi.edu/wp-content/uploads/2021/08/graphics-SIPEX_P_Kimball-IMG_9445-1920x1280-1-2-1200x679.jpg"
  )
  |> elem(1)
  |> CImg.load()

CImg.to_jpeg(img)
|> Kino.Image.new(:jpeg)
```

<img src="https://user-images.githubusercontent.com/1754093/149272096-80ce6724-c14e-443c-821c-62f71cfc9350.jpg" width="90%" />

`Midas.apply/1`は結果としてモノクロ画像を出力する。それをそのまま表示するのはなんとも無粋な眺めである。そこで、少々色気を付け加えたいと思う。cimg_ex v0.1.6では未公開の関数を用いて、モノクロ画像をカラフルなヒートマップ画像に変換する関数`heatmap/1`を用意しよう。

```elixir
heatmap = fn img ->
  with {:ok, data} = CImg.NIF.cimg_map_color(img, "jet", 0) do
    %CImg{handle: data}
  end
end
```

***MiDaS***深度推定の実行結果は下の様になる。一番手前のペンギンが濃いオレンジで、奥に行くほど青に変わっていくのがわかる。期待通りの動作しているようだ。

```elixir
Midas.apply(img)
|> CImg.resize({1200, 679})
|> heatmap.()
|> CImg.to_jpeg()
|> Kino.Image.new(:jpeg)
```

<img src="https://user-images.githubusercontent.com/1754093/149272225-33547dc8-98a4-4b11-97da-377bc8577caf.jpg" width="90%" />

## 5.Epilog

さて、一応それらしく動作するデモが実現できたかなと思う。
まだきちんと理解していないのだが、***MiDaS***が推定する深度は絶対的なモノではなく、相対的でシーン中のオブジェクトの前後関係が分ると言った程度のモノの様に思う。とは言え、たった一枚の写真から、それも短時間にここまで深度を推定できるとは、20世紀の技術者である小生とっては驚異である。それにしても、いったいどういう原理で深度を推定しているのだろう。もっともっと勉強せねば(^^;)。

(完)

## 補足１ - tfliteモデルのプロファイル確認機能について

`Midas`モジュールが利用可になった後ならば、次の様にして tfliteモデルのプロファイルを確かめることができる。

```elixir
TflInterp.info(Midas)
```

```elixir
{:ok,
 %{
   "XNNPack" => true,
   "class" => 0,
   "exe" => "/home/shoz/.cache/mix/installs/elixir-1.13.1-erts-12.1.4/52d5348d716c5977338f33f965dd9349/_build/prod/lib/tfl_interp/priv/tfl_interp",
   "inputs" => [%{"dims" => [1, 256, 256, 3], "name" => "Const", "type" => "FLOAT32"}],
   "label" => "none",
   "model" => "model_opt.tflite",
   "outputs" => [
     %{
       "dims" => [1, 256, 256, 1],
       "name" => "midas_net_custom/sequential/re_lu_9/Relu",
       "type" => "FLOAT32"
     }
   ],
   "thread" => 4
 }}
```

## 補足２ - Ubuntuに最新のCMakeをインストールする手順
Ubuntuへの CMake verion 3.18以降のインストールは、残念ながら`apt install cmake`ではできない。以下の手順に沿って CMakeのサイトからソースコードをダウロードして、手元でビルド&インストールすることになる。念の為に、`bash`の hashをリセットしておこう。

```shell
~/demo$ wget https://github.com/Kitware/CMake/releases/download/v3.22.1/cmake-3.22.1.tar.gz
~/demo$ tar xvzf cmake-3.22.1.tar.gz
~/demo$ cd cmake-3.22.1
~/demo/cmake-3.22.1$ ./bootstrap
~/demo/cmake-3.22.1$ make
~/demo/cmake-3.22.1$ sudo make install
~/demo/cmake-3.22.1$ hash -r
```
cmakeのバージョン確認方法は次の通り。

```
~/demo$ cmake --version
cmake version 3.22.1

CMake suite maintained and supported by Kitware (kitware.com/cmake).
```
無事インストールできたようだ:wink:

参考:本家CMakeサイト
1. インストール手順 "[Installing CMake](https://cmake.org/install/)"
2. ダウンロード・ファイル置き場所 "[Get the Software](https://cmake.org/download/)"