EvisionのDNN.DetectionModelでYOLOv4を使って物体検知

# はじめに
本記事は [Qiita AdventCalendar2022 Elixir](https://qiita.com/advent-calendar/2022/elixir) vol2 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.DetectionModelを使って物体検知を行う方法を紹介します

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


# 物体検知について
> 物体検出（ObjectDetection）とは、画像の中から物体らしき領域を検出し、その物体がカップなのかペンなのかといったクラスに分類するタスクです。
OpenCVではじめよう ディープラーニングによる画像認識 p.350

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

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

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

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


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

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

```elixir
alias Evision, as: Ev
base = "７．3をダウンロードしたフォルダの絶対パスを貼り付けてください"
weights = base <> "7.3/yolov4/yolov4.weights"
config = base <> "7.3/yolov4/yolov4.cfg"
```
`classes`で推論結果のラベル名を取得するためにクラス名のリストを作ります
`colors`で推論結果を囲うためにラベル毎にランダムな色のリストを作ります

```elixir
classes = File.read!(base <> "7.3/yolov4/coco.names") |> String.split("\n")
colors =
  Enum.map(classes, fn _ ->
    {Enum.random(0..255), Enum.random(0..255), Enum.random(0..255)}
  end)
```

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

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

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

# 推論の実行

画像を読み込みます
YOLOでよく使われる犬の画像を使います
```elixir
image = Ev.imread(base <> "7.3/yolov4/dog.jpg")
```

![dog.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/82fe31c8-f721-8a4b-e38b-ba199ec8876d.jpeg)


`detect/3`で推論を実行します
オプションはそれぞれ
- confThreshold -> 信頼度0.5以上のみ対象とする
- nmsTreshhold -> NMS(Non-Maximum Suppression)の信頼度0.4以上のみを対象とする
としています

```elixir
{class_ids, confidences, boxes} =
  Ev.DNN.DetectionModel.detect(model, image, confThreshold: 0.5, nmsThreshold: 0.4)
```

実行すると検出したオブジェクトのクラスID、信頼度、座標のリストがそれぞれ返ってきます

```elixir
{[1, 7, 16], [0.9896954298019409, 0.912635087966919, 0.9911838173866272],
 [{129, 131, 439, 293}, {465, 75, 227, 96}, {133, 234, 177, 302}]}
```

# 推論結果を画像に描画
Enum.zip_reduceで推論結果を描画していきます

`zip_reduce`は複数のリストを受け取って、各リストの１番目からをまとめて１つのリストにしてループを回してアキュムレータのimageに対して推論結果を描画します

boxはそれぞれ`{x,y,w,h}`に展開します

短形を描画する関数rectangleの引数はそれぞれ
1. 画像
2. 始点座標 {x,y}
3. 終点座標 {x + w, y + h}
4. 色(クラス毎にランダム作成した色)
5. オプション、今回は線の太さ

クラス名のテキストを描画するputTextの引数はそれぞれ

1. 画像
2. テキスト(クラス名と信頼度小数点３桁)
3. 座標(始点ちょっと上) 
4. フォント
5. 文字の大きさ
6. 色 
7. オプション、今回は線の太さ

となっています

```elixir
Enum.zip_reduce([class_ids, confidences, boxes], image, fn [id, conf, {x, y, w, h}], mat ->
  Ev.rectangle(mat, {x, y}, {x + w, y + h}, Enum.at(colors, id), thickness: 2)
  |> Ev.putText(
    "#{Enum.at(classes, id)}:(#{Float.round(conf, 3)})",
    {x, y - 5},
    Ev.cv_FONT_HERSHEY_SIMPLEX(),
    0.5,
    {0, 0, 0},
    thickness: 2
  )
end)
```

![スクリーンショット 2022-12-04 19.00.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/3f8bc942-7444-54dd-40e5-00e92edcf2ac.png)


# 最後に
DetectionModelを使用して簡単に物体検出ができました

他のバージョンのYOLOも動くと思うので色々試してみてください
本記事は以上になりますありがとうございました


# 全コード

```elixir
alias Evision, as: Ev
base = "/Users/shou/livebook_samples/"
weights = base <> "7.3/yolov4/yolov4.weights"
config = base <> "7.3/yolov4/yolov4.cfg"
classes = File.read!(base <> "7.3/yolov4/coco.names") |> String.split("\n")
colors =
  Enum.map(classes, fn _ ->
    {Enum.random(0..255), Enum.random(0..255), Enum.random(0..255)}
  end)

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

image = Ev.imread(base <> "7.3/yolov4/dog.jpg")
{class_ids, confidences, boxes} =
  Ev.DNN.DetectionModel.detect(model, image, confThreshold: 0.5, nmsThreshold: 0.4)

Enum.zip_reduce([class_ids, confidences, boxes], image, fn [id, conf, {x, y, w, h}], mat ->
  Ev.rectangle(mat, {x, y}, {x + w, y + h}, Enum.at(colors, id), thickness: 2)
  |> Ev.putText(
    "#{Enum.at(classes, id)}:(#{Float.round(conf, 3)})",
    {x, y - 5},
    Ev.cv_FONT_HERSHEY_SIMPLEX(),
    0.5,
    {0, 0, 0},
    thickness: 2
  )
end)
```