YOLOv4の結果を切り取ってEfficientnetで更に分類するシステムをEvisionで書く

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

Evisionシリーズの7つ目の記事になります
このシリーズは技術評論社の[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.DetectionModelとDNN.ClassificationModelを組み合わせて
YOLOv4の結果をEfficientnetで更に分類する、簡易アノテーションツールを作ります。

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


# アノテーションツールについて
> AIアノテーションツールとは、AIの精度を高めるために必要な教師データ（訓練データ）の作成作業、すなわちアノテーションを自動化もしくは効率化できるツールのことを指します。
https://www.aspicjapan.org/asu/article/16061

アノテーションツールに種類が色々あるみたいですが、今回はいろんな画像からクラス分類に使えそうな訓練データを作るツールとしましょう

流れとしては以下になります
1. YOLOv4で物体検知してBBox(対象が写っている領域)を取得
2. １の領域を切り取って個別の画像にする
3. ２の画像を１つずつEfficientnetで分類する
4. 分類結果のクラス名をファイル名として切り取った画像を書き出す


# 使用するモデル
YOLOv4とEfficientnet-b7を使います
以下から7.3と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.3.zip
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"},
])
```

# 物体検知部分

```elixir
alias Evision, as: Ev
base = "your 7.3 donwloaded folder abs path"
image = base <> "7.3/yolov4/dog.jpg"
weights = base <> "7.3/yolov4/yolov4.weights"
config = base <> "7.3/yolov4/yolov4.cfg"

dog = Ev.imread(image) |> Ev.resize({416, 416})

model =
  Ev.DNN.DetectionModel.detectionModel(weights, config: config)
  |> Ev.DNN.DetectionModel.setInputParams(
    scale: 1.0 / 255.0,
    size: {416, 416},
    mean: {0, 0, 0},
    swapRB: true,
    crop: false
  )

{_class_ids, _confidences, boxes} =
  Ev.DNN.DetectionModel.detect(model, dog, confThreshold: 0.5, nmsThreshold: 0.4)

images =
  Enum.map(boxes, fn {x, y, w, h} ->
    Ev.Mat.roi(dog, {x - 10, y - 10, w + 10, h + 10})
  end)
```

基本的にDNN.DetectionModelと同じで、Enum.zip_reduceで画像に書き込む部分がなくて
Ev.Mat.roiで指定した座標内のみのRGB値の画像にします

どんな感じになったか見てみましょう

```elixir
to_img = fn img -> Ev.imencode(".png", img) |> Kino.Image.new(:png) end  
Enum.map(images, &to_img.(&1)) |> Kino.Layout.grid(columns: 3)
```

![スクリーンショット 2022-12-05 22.29.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/4d9fa244-1c6b-1f4d-50fc-cf2acead62c3.png)

ちゃんと切り取られてますね
次はこれを使ってクラス分類を行います

# クラス分類部分
データは物体検知と分けるためにそれぞれ
c_weigths,c_modelで定義します

tap関数は第１引数を受け取り、何かしら処理を行い、第１引数の値をそのまま帰す関数です
今回は切り取った画像データを書き出すのに使っています

本来は 画像を書き出す`Evision.imwrite`だけでいいのですが、推論結果を確認するために`tap`を挟んで、推論結果を画像に書き込んで表示するようにしています

```elixir
c_weights = base <> "7.4/efficientnet/efficientnet-b7.onnx"

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

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

images = 
Enum.map(images, fn image ->
  {class_id, confidence} =
    Ev.DNN.ClassificationModel.classify(c_model, image)

  name = Enum.at(classes, class_id) |> String.split(",") |> List.first()

  Ev.resize(image, {412, 412}, interpolation: Ev.cv_INTER_NEAREST())
  |> tap(&Ev.imwrite("images/#{name}.png", &1))
  |> Ev.putText(
    "#{name}(#{Float.round(confidence, 3)})",
    {50, 50},
    Ev.cv_FONT_HERSHEY_DUPLEX(),
    0.8,
    {0, 0, 255}
  )
end)
|> Enum.map(&to_img.(&1))
|> Kino.Layout.grid(columns: 3)
```
推論結果はこんな感じ
![スクリーンショット 2022-12-05 23.31.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/172c18be-59f9-760b-7008-255cf90ac881.png)

画像も問題なく書き出されてます
![スクリーンショット 2022-12-05 23.32.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/766e3310-94ac-834a-f3b0-17e80400fba9.png)

自転車の画像で犬に分類されたり、犬の画像で別の犬種が出たりと微妙な結果ですが、
もうちょっといいモデルとかにすれば改善できるといいな・・・


# 最後に
DNN.DetectModelとDNN.ClassificationModelを組み合わせて簡易的なアノテーションツールを作ってみました

Elixirはパイプ演算子で処理を繋げれるので、複数のDNNの処理をつなげるのが見通しよくかけるのがとても気に入っています

またLivebookはデスクトップアプリもあるので、livemdをそのまま渡したり、ElixirDesktopにEvisionを組み込んでもうちょっとちゃんとしたアノテーションツールにしたりなど色々できそうですね


本記事は以上になりますありがとうございました


# 全コード

```elixir
alias Evision, as: Ev
base = "/Users/shou/livebook_samples/"
weights = base <> "7.3/yolov4/yolov4.weights"
config = base <> "7.3/yolov4/yolov4.cfg"
c_weights = base <> "7.4/efficientnet/efficientnet-b7.onnx"
c_classes =
  (base <> "7.4/efficientnet/imagenet.names")
  |> File.read!()
  |> String.split("\n")

model =
  Ev.DNN.DetectionModel.detectionModel(weights, config: config)
  |> Ev.DNN.DetectionModel.setInputParams(
    scale: 1.0 / 255.0,
    size: {416, 416},
    mean: {0, 0, 0},
    swapRB: true,
    crop: false
  )

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

{_class_ids, _confidences, boxes} =
  Ev.DNN.DetectionModel.detect(model, dog, confThreshold: 0.5, nmsThreshold: 0.4)

boxes
|> Enum.map(fn {x, y, w, h} ->
  Ev.Mat.roi(dog, {x - 10, y - 10, w + 10, h + 10})
end)
|> Enum.map(fn image ->
  {class_id, _confidence} =
    Ev.DNN.ClassificationModel.classify(c_model, image)
  name = Enum.at(classes, class_id) |> String.split(",") |> List.first()
  Ev.resize(image, {412, 412}, interpolation: Ev.cv_INTER_NEAREST())
  |> tap(&Ev.imwrite("images/#{name}.png", &1))
end)
```