Livebook + Evision基本編 Evision(OpenCV)でお手軽画像処理

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

Evisionシリーズの1つ目の記事になります
このシリーズは技術評論社の[OpenCVではじめよう ディープラーニングによる画像認識](https://gihyo.jp/book/2022/978-4-297-12775-6)の3章の内容を参考に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を使うときの基本的な関数の使い方を紹介します

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


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


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


# 黒色画像の作成 -- Mat.zeros
Mat.zerosは第一引数で指定したサイズで0で埋めた行列を作成します
ついでにモジュール名をaliasで短くします
```elixir
alias Evision, as: Ev

Ev.Mat.zeros({100, 100}, :u8)
```
![スクリーンショット 2022-11-25 23.32.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/2427397d-bd74-7c78-fdb6-8b186e562c2e.png)
RGBの値で作ってMatで表示したい場合は Mat.from_nx_2dを経由する必要があるようです

```elixir
Ev.Mat.zeros({100, 100, 3}, :u8) 
```
![スクリーンショット 2022-11-25 23.40.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/4366fdb2-a25f-5268-62dc-2eb4c9a9688d.png)

```elixir
Ev.Mat.zeros({100, 100, 3}, :u8) 
|> Ev.Mat.to_nx 
|> Ev.Mat.from_nx_2d()
```

![スクリーンショット 2022-11-25 23.42.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/2ae872d7-e86f-211c-0f80-f5b5b5c8cb4b.png)

# グレー画像の作成 -- Mat.full
Mat.fullは第２引数で指定した値で埋めた第１引数のサイズの行列を作ります

```elixir
Ev.Mat.full({100, 100}, 200, :u8)
```

# Nxから色画像の作成 -- Mat.from_nx_2d
Mat.fullだと単色画像はできないので、Nx.broadcastを使って作成していきます
Nx.broadcastの例を見ても分かりづらいですが
Nx.tensorで作った行列の要素数が合うようにshapeを指定すると
単色のRGBの行列を作ることができます

```elixir
Nx.broadcast(200,{100,100})
```
```elixir
#Nx.Tensor<
  s64[100][100]
  [
    [200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, ...],
    ...
  ]
>

```
shapeが一致しないとエラー
```elixir
Nx.tensor([0,0,255])
|> Nx.broadcast({100,100,5})
```
```elixir
** (ArgumentError) cannot broadcast tensor of dimensions {3} to {100, 100, 1} with axes [2]
```
NxからEv.Mat.from_nxするときの注意点ですが
Nxのデフォルトのタイプは :f16でそれだと　Evisionでは対応していないので
:u8か:f32にタイプを指定してください
今回はRGB値なので:u8を指定します

```elixir
shape = {250, 250, 3}

red =
  Nx.tensor([0, 0, 255], type: :u8)
  |> Nx.broadcast(shape)
  |> Ev.Mat.from_nx_2d()

green =
  Nx.tensor([0, 255, 0], type: :u8)
  |> Nx.broadcast(shape)
  |> Ev.Mat.from_nx_2d()

blue =
  Nx.tensor([255, 0, 0], type: :u8)
  |> Nx.broadcast(shape)
  |> Ev.Mat.from_nx_2d()

to_img = fn img -> Ev.imencode(".png", img) |> Kino.Image.new(:png) end

Kino.Layout.grid(
  [to_img.(red), to_img.(green), to_img.(blue)],
  columns: 3
)
```

複数の画像を表示する際の小技ですが、
Ev.encodeでMatのデータをpng形式に変換し
Kino.Image.new()で　png形式のKino.Imageを作成
Kino.Layout.gridの第１引数にリストで渡して表示する
デフォルトだと縦方向なので:columnsオプションを指定すると以下のように表示できます

![スクリーンショット 2022-11-26 0.12.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/b67a254e-a0a4-281f-93b6-738b6008b89c.png)

# 画像の読み込み -- imread
git cloneしてきたlivebookで起動しているので、cwdはlivebook直下になります
static/imagesにロゴ画像があるのでそちらを読み込みます

```elixir
img = "static/images/logo.png" |> Ev.imread()
```
![スクリーンショット 2022-11-28 14.22.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/f30c19a2-03d9-6a6b-5cd5-374c175b92d5.png)

# ファイル書き込み -- imwrite
ファイルを書き込む時は適切な拡張子をつけていれば、それに沿って画像ファイルを生成します
```elixir
Ev.imwrite("images/logo.png", img)
```

```elixir
Ev.imwrite("images/logo.jpg", img)
```

![スクリーンショット 2022-11-30 11.37.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/0ce1d197-49b5-51dd-052e-e1fc2622a6b1.png)

拡張子を指定しなければエラーになります
```elixir
Ev.imwrite("images/logo", img)
```

```elixir
{:error, "OpenCV(4.6.0) /Users/runner/work/evision/evision/3rd_party/opencv/opencv-4.6.0/modules/imgcodecs/src/loadsave.cpp:730: error: (-2:Unspecified error) could not find a writer for the specified extension in function 'imwrite_'\n"}
```
# リサイズ -- resize
第２引数に縦横のタプルを渡します
```elixir
img = Ev.resize(img, {200, 200})
```
![スクリーンショット 2022-11-30 11.38.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/ca241654-63ea-d6ec-3c31-8738dde6263d.png)


# 回転 -- rotate
時計回りに90度
```elixir
Ev.rotate(img, Ev.cv_ROTATE_90_CLOCKWISE())
```
反時計回りに９０度
```elixir
Ev.rotate(img, Ev.cv_ROTATE_90_COUNTERCLOCKWISE())
```
180度回転
```elixir
Ev.rotate(img, Ev.cv_ROTATE_180())
```

![スクリーンショット 2022-11-30 11.41.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/1bc1e9ef-2013-ed92-0282-8673e0ef6420.png)

# 反転 -- flip

```elixir
# 縦方向に反転
Ev.flip(img, 0)
# 横方向に反転
Ev.flip(img, 1)
```
![スクリーンショット 2022-11-30 11.46.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/3258bcd4-6765-9c29-6fef-bf8da2a641fe.png)


# 連結 -- hconcat, vconcat
横連結
```elixir
Ev.hconcat([img, img])
```
![スクリーンショット 2022-11-30 11.51.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/27b6cf06-a6bd-37ea-6ae4-d6ba97c99d2b.png)

縦連結
```elixir
Ev.vconcat([img, img])
```

![スクリーンショット 2022-11-30 13.23.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/349eb186-1d1c-25dc-d457-73b44ff1c8ab.png)

結合箇所のサイズ（横連結の場合は高さ）が違うとエラーになります

```elixir
Ev.hconcat([img, Ev.resize(img, {100, 100})])
```

```elixir
{:error,"OpenCV(4.6.0) /Users/runner/work/evision/evision/3rd_party/opencv/opencv-4.6.0/modules/core/src/matrix_operations.cpp:67: error: (-215:Assertion failed) src[i].dims <= 2 && src[i].rows == src[0].rows && src[i].type() == src[0].type() in function 'hconcat'\n"}
```

結合箇所以外はサイズが違っても問題なく結合できます

```elixir
Ev.hconcat([img, Ev.resize(img, {100, 200})])
```
![スクリーンショット 2022-11-30 13.27.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/928d4587-b86a-c887-5257-cb88cf5c8b16.png)


# 短形図形を描く -- rectangle
短形（四角）を画像内に描画します、物体検知(YOLOとか)でよく使うあれです
引数はそれぞれ
- 1 image
- 2 始点(x, y)
- 3 終点(x+w, y+h)
- 4 色
- 5 オプションいろいろ、　今回は線の太さ

```elixir
{x, y, w, h} = {25, 25, 50, 50}
color = {255, 255, 255}
Ev.rectangle(img, {x, y}, {x + w, y + h}, color, thickness: 3)
```

![スクリーンショット 2022-11-30 13.41.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/6abc94f8-09e9-9d78-7f7b-7a29992db8e6.png)

thicknessを-1にすると塗りつぶしになります
```elixir
Ev.rectangle(img, {x, y}, {x + w, y + h}, color, thickness: -1)
```
![スクリーンショット 2022-11-30 13.44.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/1b8b00f6-ce94-3506-568d-c8f0c7db0a63.png)

# 円を描く -- circle, ellipse
- 1 画像
- 2 中心座標
- 3 半径
- 4 色
- 5 その他オプション

```elixir
Ev.circle(img, {100, 100}, 40, color, thickness: 3)
```
![スクリーンショット 2022-11-30 13.48.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/88b18e5e-9ced-a4c5-5ae9-7bd2d3938a58.png)

ellipseは楕円を描くときに使います
- 1 画像
- 2 中心座標
- 3 {縦幅、横幅}
- 4 回転
- 5 描画位置始点
- 6 描画位置終点
- 7 色
- 8 その他オプション

```elixir
Ev.ellipse(img, {100, 100}, {50, 100}, 60, 0, 360, color)
```
![スクリーンショット 2022-11-30 13.51.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/b1c35df7-e716-13f4-6004-7a8960cbdd8f.png)

幅を揃えれば円になりますし
90度回転させて、描画位置を0~180に指定すると半円になります

```elixir
Ev.ellipse(img, {100, 100}, {50, 50}, 90, 0, 180, color, thickness: -1)
```
![スクリーンショット 2022-11-30 13.52.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/f9c511cf-73f6-727f-7a8c-8411a2f6dbc6.png)

# 文字を入れる -- putText
画像の指定座標に文字を描画します
- 1 画像
- 2 入れる文字
- 3 {x座標,y座標}
- 4 フォント
- 5 文字の大きさ
- 6 文字の色

```elixir
Ev.putText(img, "LiveViewJP", {30, 50}, Ev.cv_FONT_ITALIC(), 1.0, color)
```
![スクリーンショット 2022-11-30 14.09.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/b71ac92b-5932-2903-f388-974358206ee6.png)

# 切り取り -- Mat.roi 
roiは関心領域(Region of Interest)の略称だそうです
指定した領域を切り抜きます
- 1 画像
- 2 {x, y, x + w, y + h}

```elixir
Ev.Mat.roi(img, {50, 50, 100, 100})
```
![スクリーンショット 2022-11-30 13.58.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/2baed3ea-c397-1c2f-b0db-8f14afb64248.png)

# アフィン変換 -- warpAffine
最後は名前だけはよく聞くアフィン変換です
使いこなすには行列計算の知識とかいるのでちょっとハードル高そうですね

> 画像の拡大縮小、回転、平行移動などを行列を使って座標を変換する事をアフィン変換と呼びます。 by https://imagingsolution.net/imaging/affine-transformation/
```elixir
src_img = img
{width, height} = {200, 200}

# 並行移動
x = 50
y = -10
m_shift = Nx.tensor([[1, 0, x], [0, 1, y]], type: :f32) |> Ev.Mat.from_nx()
sheer_img = Ev.warpAffine(src_img, m_shift, {width, height})

# 回転
angle = 45
m_rotate = Ev.getRotationMatrix2D({width / 2, height / 2}, angle, 1.0)
rotation_img = Ev.warpAffine(src_img, m_rotate, {width, height})

# スキュー(平行四辺形に変形する処理)
a = 0.2
b = 0.0
m_shear = Nx.tensor([[1, a, 0], [b, 1, 0]], type: :f32) |> Ev.Mat.from_nx()
shear_img = Ev.warpAffine(src_img, m_shear, {width, height})
```

![スクリーンショット 2022-11-30 14.05.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/1d61a0b7-14e2-ce92-90e7-c581c0f3e1e3.png)