EvisionのDNN.ClassificationModelを使ってクラス分類をする

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

Evisionシリーズの２つ目の記事になります
このシリーズは技術評論社の[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.ClassificationModelを使ってクラス分類を行う方法を紹介します

# 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分野の多くの学習済みモデルを使用できる


# クラス分類について
> クラス分類（Classfication）とは、画像に何が映っているのかをクラスごとに信頼度を出して分類するタスクです。
by OpenCVではじめよう ディープラーニングによる画像認識 (p.380)

# 使用するモデル
Efficientnet-b7.onnx　を使用します
クラスリストと学習済みデータはこちらの7.4をダウンロードしてください

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.4.zip

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


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

# データの準備
`alias`でモジュール名を短くしています `import numpy as np`と同じですね
`weigths`で学習済みデータのファイルパスを定義します
`base`でダウンロードしたフォルダパスを貼っておくと便利です
`classes`でクラス分類の推論結果は学習時に使用した正解リストのインデックスを返すので、クラス名がわかるように正解リストをリストにします

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

weights = base <> "7.4/efficientnet/efficientnet-b7.onnx"

classes =
  (base <> "7.4/efficientnet/imagenet.names")
  |> File.read!()
  |> String.split("\n")
```

# モデルの読み込み
学習済みデータを読み込んで、モデルを構築します
`classificationModel/1`で先程の学習済みデータのパスを渡します
次に`setInputParams/2`でハイパーパラメータを設定します
パラメーターはそれぞれ以下を設定しています
- scale -> 画像のRGB値を0~1.0の範囲に変換
- size -> 入力画像サイズを600x600にする
- mean -> 各RGB値から指定した平均値を引いて照度変化に強くする
- swapRB -> BGR形式をRGBに入れ替える
- crop -> アスペクト比を保持してリサイズする

```elixir
model =
  Ev.DNN.ClassificationModel.classificationModel(weights)
  |> Ev.DNN.ClassificationModel.setInputParams(
    scale: 1.0 / 255.0,
    size: {600, 600},
    mean: {124.675, 116.28, 103.53},
    swapRB: true,
    crop: true
  )
```

# 推論の実行

画像を読み込みます
使うのはYOLOでおなじみの犬と自転車が写っているアレです
```elixir
image = Ev.imread(base <> "images/dog.jpg")
```
![dog.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/e19f3e80-32e1-4480-8f77-07f4467daf86.jpeg)

`classify/2`で推論を実行します

```elixir
{class_id, confidence} = Ev.DNN.ClassificationModel.classify(model, image)
```

実行するとクラスIDと正当確率が返ってきます

```elixir
{249, 8.626869201660156}
```

# 推論結果を画像に描画
推論結果をクラスIDからクラス名に変換し、確率も小数点以下３まで表示するようにして、`putText/6`で元画像に描画します

putTextの引数はそれぞれ以下のようになっています
- 画像
- 書き込むテキスト
- 書き込む座標(左下)
- フォント
- 文字の大きさ
- 文字の色

```elixir
Ev.putText(
  image,
  "#{Enum.at(classes, class_id)}(#{Float.round(confidence, 3)})",
  {30, 30},
  Ev.cv_FONT_HERSHEY_TRIPLEX(),
  1.0,
  {255, 255, 255}
)
```

![スクリーンショット 2022-12-02 13.34.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/c1bb81a0-2694-b7f5-5a72-1d337882f1ae.png)

# おまけ上位５つを表示

他の候補も見たい場合は `predict/2` を使います、
処理は上から
1. 推論実行
2. 結果がMatのリスト([Mat])で返ってくるので１つ目の要素を取得
3. Matだと行列処理がやりにくいのでNxに変換
4. shapeが{1, 1000} なので squueeze で {1000}に変換
5. 出力されたデータを0~1の範囲の確率の値に変換(Softmax)

となっています

```elixir
confidences =
  Ev.DNN.ClassificationModel.predict(model, image)
  |> List.first()
  |> Ev.Mat.to_nx(Nx.BinaryBackend)
  |> Nx.squeeze()
  |> then(&Nx.divide(
    Nx.exp(&1), 
    Nx.sum(Nx.exp(&1))
  ))
```
上記のデータはクラス毎の確率になっているので、以下の処理を行います

1. `Nx.argsort direction: :desc`で大きい順のインデックスに並び替えます
2. `Enum.take(5)`で先頭から５つでリストを作ります
3. `Enum.map`で　クラス名と確率のタプルに変換


```elixir
top5 =
  Nx.argsort(confidences, direction: :desc)
  |> Nx.to_flat_list()
  |> Enum.take(5)
  |> Enum.map(fn c ->
    {Enum.at(classes, c), Nx.to_number(confidences[c])}
  end)
```

実行すると以下のような結果を得られます

```elixir
[
  {"malamute, malemute, Alaskan malamute", 0.7833796739578247},
  {"Tibetan mastiff", 0.017758667469024658},
  {"mountain bike, all-terrain bike, off-roader", 0.015341623686254025},
  {"Eskimo dog, husky", 0.006293073762208223},
  {"Siberian husky", 0.0028612350579351187}
]
```



# 最後に
ClassificationModelを使用して簡単にクラス分類を行うことができました
他にも試してみたい方はmodel zooやOpenCV zoo で色々試してみてください
本記事は以上になりますありがとうございました

https://github.com/opencv/opencv_zoo

https://github.com/openvinotoolkit/open_model_zoo

https://github.com/onnx/models/tree/main/vision/classification


# 全コード

```elixir
alias Evision, as: Ev
base = "/Users/shou/livebook_samples/"

weights = base <> "7.4/efficientnet/efficientnet-b7.onnx"

classes =
  (base <> "7.4/efficientnet/imagenet.names")
  |> File.read!()
  |> String.split("\n")

model =
  Ev.DNN.ClassificationModel.classificationModel(weights)
  |> Ev.DNN.ClassificationModel.setInputParams(
    scale: 1.0 / 255.0,
    size: {600, 600},
    mean: {124.675, 116.28, 103.53},
    swapRB: true,
    crop: true
  )

image = Ev.imread(base <> "images/dog.jpg")
{class_id, confidence} = Ev.DNN.ClassificationModel.classify(model, image)

Ev.putText(
  image,
  "#{Enum.at(classes, class_id)}(#{Float.round(confidence, 3)})",
  {30, 30},
  Ev.cv_FONT_HERSHEY_TRIPLEX(),
  0.5,
  {255, 255, 255}
)
```