EvisionのDNN.SegmentationModelを使ってセグメンテーション

# はじめに
本記事は [Qiita AdventCalendar2022 Elixir](https://qiita.com/advent-calendar/2022/elixir) vol9 14日目の記事です

Evisionシリーズの5つ目の記事になります
このシリーズは技術評論社の[OpenCVではじめよう ディープラーニングによる画像認識](https://gihyo.jp/book/2022/978-4-297-12775-6)の7章の内容を参考にElixirとEvisionで書き換えて行っています

https://gihyo.jp/book/2022/978-4-297-12775-6

1. [Livebook + Evision基本編](https://qiita.com/the_haigo/items/baf47624567cb992d934)
4. [EvisionのCascadeClassifierで顔認識](https://qiita.com/the_haigo/items/f1ffbdcb875eb83e52f4)
2. [EvisionのDNN.ClassificationModelを使ってクラス分類](https://qiita.com/items/8fdbb80bdff8ff6fd2fc/)
3. [EvisionのDNN.TextDetectionModelDBでテキスト検出](https://qiita.com/items/6b177d2c85625ace131a)
5. [EvisionのDNN.DetectionModelでYOLOv4を使って物体検知](https://qiita.com/the_haigo/items/3f8b4d1801cdbba3bfe8) 12/14公開
6. [EvisionのDNN.SegmentationModelでセグメンテーション](https://qiita.com/the_haigo/items/c99a6ca3b51d18e3f127) 12/14公開
7. [YOLOv4の結果を切り取ってEfficientnetで更に分類するシステムをEvisionで書く](https://qiita.com/the_haigo/items/7e426f2176f7a6535320) 12/18公開

Livebook上で画像処理ライブラリOpenCVのElixirラッパーのEvisionのDNN.SegmentationModelを使ってセグメンテーションを行う方法を紹介します

# Livebookについて
https://livebook.dev/

https://github.com/livebook-dev/livebook

> Livebook is a web application for writing interactive and collaborative code notebooks.

LivebookはコラボレーションもできるElixir対話的実行環境を提供するWebアプリケーションです

# Evisionについて
https://github.com/cocoa-xu/evision


* OpenCVのElixirラッパー
* Port等を使わずに直にElixirから使うことができる
* Nxのバックエンドとして使用でき、行列演算の高速化(CPU,GPU)ができる
* Nxデータに相互に変換できる
* 膨大な画像処理の関数を使用できる
* DNNモジュールでCV分野の多くの学習済みモデルを使用できる


# セグメンテーションについて
> セグメンテーション（Segmentation）とは、画像に映っているオブジェクトのクラスごとに領域を分割するタスクです。
OpenCVではじめよう ディープラーニングによる画像認識 (p.410)

使用するモジュールはDNN.DetectionMoelになります

# 使用するモデル
deeplab-v3
学習済みデータはこちらの7.5をダウンロードしてください

https://gihyo.jp/book/2022/978-4-297-12775-6/support

直リンク
https://gihyo.jp/assets/files/book/2022/978-4-297-12775-6/download/7.5.zip

# setup
livebookは公式サイトを参考にインストールしてください
livebookを起動してノートブックを作成したらsetupセルに以下を追加して実行してください


```elixir
Mix.install([
  {:evision, "~> 0.1.21"},
  {:kino, "~> 0.7.0"},
])
```

# データの準備
`alias`でモジュール名を短くしています `import numpy as np`と同じですね
`weigths`で学習済みデータのファイルパスを指定します
`base`でダウンロードしたフォルダパスを貼っておくと便利です

```elixir
alias Evision, as: Ev
base = "７．3をダウンロードしたフォルダの絶対パスを貼り付けてください"
weights = base <> "7.5/deeplab-v3/optimized_graph_voc.pb"
```
`classes`で推論結果のラベル名を取得するためにクラス名のリストを作ります
`colors`で推論結果を囲うためにラベル毎にランダムな色のリストを作ります
色リストの一番最初はラベル外なので黒を入れておきます
またあとでNx.takeで使うので Nx.tensorでテンソル化しておきます

```elixir
classes =
  (base <> "7.5/deeplab-v3/voc.names")
  |> File.read!()
  |> String.split("\n")

colors =
  Enum.with_index(classes, fn _class, index ->
    case index do
      0 -> [0, 0, 0]
      _ -> [Enum.random(0..255), Enum.random(0..255), Enum.random(0..255)]
    end
  end)
  |> Nx.tensor()
```

# モデルの読み込み
学習済みデータを読み込んで、モデルを構築します
`segmentationModel/1`で先程の学習済みデータとオプションでネットワークの設定ファイルのパスを渡します

モデルの次は`setInputParams/2`でハイパーパラメータを設定します
パラメーターはそれぞれ以下を設定しています
- scale -> meanを引いた後に画像のRGB値を0~1.0の範囲に変換
- size -> 入力画像サイズを513x513にする
- mean -> 入力画像の各RGB値から127.5を引く
- swapRB -> BGR形式をRGBに入れ替える
- crop -> アスペクト比をせずにリサイズ

```elixir
model =
  Ev.DNN.SegmentationModel.segmentationModel(weights)
  |> Ev.DNN.SegmentationModel.setInputParams(
    scale: 1.0 / 127.5,
    size: {513, 513},
    mean: {127.5, 127.5, 127.5},
    swapRB: true,
    crop: false
  )
```

# 推論の実行

画像を読み込みます
書籍のサンプルにあった自転車の画像を使います

```elixir
image = Ev.imread(base <> "7.5/deeplab-v3/bicycle.jpg")
```

![bicycle.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/b40e7a29-f575-f8e0-9070-aa335b49984a.jpeg)


`segment/3`でセグメンテーションを実行します

```elixir
mask =
  Ev.DNN.SegmentationModel.segment(model, image)
```

各画素のクラスIDが返ってきますが、画像としてはRGBではなく、濃淡になってしまいます

![スクリーンショット 2022-12-05 16.18.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/e2dcdf8d-c7cb-9a8c-a3c9-98730be36bb9.png)

こちらを上の方で定義したランダムの色リストに変換します
流れは以下のようになっています

1. Evision.MatからNx.tensorに変換
2. Evision.Backendだとtakeが使えないのでNx.BinaryBackendに変換する
3. Nx.takeでクラスの数作成した色から該当するインデックスの値だけでテンソルを作る
4. Evisionで扱えるようにf16からu8にデータタイプを変換
5. Evision.Mat形式に変換  

```elixir
mask = 
  mask
  |> Ev.Mat.to_nx(Nx.BinaryBackend)
  |> then(&Nx.take(colors, &1))
  |> Nx.as_type(:u8)
  |> Ev.Mat.from_nx_2d()
```
色がついてだいぶわかりやすくなりました

![スクリーンショット 2022-12-05 16.36.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/e14231c8-e62e-5f7e-3b60-c5914bc9a06f.png)


# セグメンテーション結果と画像をアルファブレンド

セグメンテーション結果から作ったマスクを半透明にして元画像に重ねます(アルファブレンド)
処理はそれぞれ以下のようになっています

1. 元画像のサイズを取得
2. マスクが513x513なので元画像とおなじになるようにリサイズ
3. オプションに補間方法（interpolation）をINTER_NEAREST(最近傍補間)で指定
4. addWeighted でアルファブレンド

```elixir
{h, w, _} = Ev.Mat.shape(image)
color_mask = Ev.resize(mask, {w, h}, interpolation: Ev.cv_INTER_NEAREST())

alpha = 0.4
beta = 1.0 - alpha
blend = Ev.addWeighted(image, 0, color_mask, beta, 0.0)
```
![スクリーンショット 2022-12-05 16.55.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/22d2e303-31c4-b86c-d9c2-99b3b2cd4b51.png)

元画像とマスクも並べてみましょう

```elixir
Kino.Layout.grid(
  [
    Ev.imencode(".png", image) |> Kino.Image.new(:png),
    Ev.imencode(".png", color_mask) |> Kino.Image.new(:png),
    Ev.imencode(".png", blend) |> Kino.Image.new(:png)
  ],
  columns: 3
)
```

![スクリーンショット 2022-12-05 16.56.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/348f8fa5-91bd-015a-ea6c-063b6ed1e09b.png)


# 最後に
SegmentationModelを使用して簡単にセグメンテーションができました

clipで切り抜きや、各種フィルターで人をぼかす、人以外をぼかす等色々できそうですね
本記事は以上になりますありがとうございました

# 全コード

```elixir
alias Evision, as: Ev
base = "７．3をダウンロードしたフォルダの絶対パスを貼り付けてください"

weights = base <> "7.5/deeplab-v3/optimized_graph_voc.pb"

classes =
  (base <> "7.5/deeplab-v3/voc.names")
  |> File.read!()
  |> String.split("\n")

colors =
  Enum.with_index(classes, fn _class, index ->
    case index do
      0 -> [0, 0, 0]
      _ -> [Enum.random(0..255), Enum.random(0..255), Enum.random(0..255)]
    end
  end)
  |> Nx.tensor()

model =
  Ev.DNN.SegmentationModel.segmentationModel(weights)
  |> Ev.DNN.SegmentationModel.setInputParams(
    scale: 1.0 / 127.5,
    size: {513, 513},
    mean: {127.5, 127.5, 127.5},
    swapRB: true,
    crop: false
  )

image = Ev.imread(base <> "7.5/deeplab-v3/bicycle.jpg")

mask =
  Ev.DNN.SegmentationModel.segment(model, image)
  |> Ev.Mat.to_nx(Nx.BinaryBackend)
  |> then(&Nx.take(colors, &1))
  |> Nx.as_type(:u8)
  |> Ev.Mat.from_nx_2d()

{h, w, _} = Ev.Mat.shape(image)
color_mask = Ev.resize(mask, {w, h}, interpolation: Ev.cv_INTER_NEAREST())

alpha = 0.4
beta = 1.0 - alpha
blend = Ev.addWeighted(image, alpha, color_mask, beta, 0.0)

Kino.Layout.grid(
  [
    Ev.imencode(".png", image) |> Kino.Image.new(:png),
    Ev.imencode(".png", color_mask) |> Kino.Image.new(:png),
    Ev.imencode(".png", blend) |> Kino.Image.new(:png)
  ],
  columns: 3
)
```