Elixir で OpenCV (Evision) を使った画像処理（移動、回転、フィルターなど）

## はじめに

これまで Python での画像処理や AI の学習・推論は実務でも扱ってきました

また、 Elixir は Phoenix を使った REST API に数年使っています

しかし、 Elixir で画像処理、 AI というのは未経験です

というわけで、 Nx と evision で Elixir での画像処理を実装してみました

実装したもの（Docker 上に環境構築）:

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/evision/image_processing.livemd

::: note info
2022/12/13 更新

最新のモジュールを使うように更新しました
:::

参考記事:

https://qiita.com/piacerex/items/533d26c81bada4741422

https://zenn.dev/kentarok/articles/4c92dacfe1d1fe

## Nx とは

Elixir で多次元配列（テンソル）を使うためのライブラリ

https://github.com/elixir-nx/nx

Python の numpy のような感覚で使えるため、画像処理に向いています

```elixir
[
  [1, 2],
  [3, 4]
]
|> Nx.tensor()
|> Nx.divide(3)
```

上記のコードを Livebook で実行するとこんな感じ

![スクリーンショット 2022-05-06 17.00.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/03c9e40f-c49a-bca0-27ef-f9e0b0d77a03.png)

テンソルの各要素が 3 で割られているのが分かりますね

## evision とは

Elixir 用の OpenCV ラッパー

https://github.com/cocoa-xu/evision

Python の OpenCV と同じことが Elixir 上で実行できます

Nx とも連携できるため、 Python と同じ感覚で画像処理できます

## 実行環境

- MacBook Pro 13 inchi
  - 2.4 GHz クアッドコアIntel Core i5
  - 16 GB 2133 MHz LPDDR3
- macOS Ventura 13.0.1
- Rancher Desktop 1.6.2
  - メモリ割り当て 12 GB
  - CPU 割り当て 6 コア

Livebook 0.8.0 の Docker　イメージを元にしたコンテナで動かしました

https://hub.docker.com/layers/livebook/livebook/0.8.0/images/sha256-6ef3fdd439fd72af9ae01d58e947664a46a8493f031e179cc0e91c08ddac02e5?context=explore

コンテナ定義はこちらを参照

https://github.com/RyoWakabayashi/elixir-learning

## 実行方法

リポジトリーをクローンして `docker-compose up` するだけです

```bash
git clone https://github.com/RyoWakabayashi/elixir-learning.git
cd elixir-learning
docker-compose up
```

ビルドが終わると以下のように localhost の URL が表示されるので、ブラウザで開いてください

```bash
...
Attaching to elixir-learning-livebook-1
elixir-learning-livebook-1  | [Livebook] Application running at http://localhost:8080/?token=xxxxx
```

こんな感じで Livebook が開きます

![スクリーンショット 2022-05-06 17.18.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/fc9da55f-f25c-3100-665e-0f2194dedda6.png)

Elixir 学習用のリポジトリーなので他のファイルもありますが、
`evision/image_processing.livemd` が本記事で紹介しているコードです

## evision の導入

### evision インストールのための Dockerfile

Docker 上では evision インストールに必要なもの + 他で使うので Phoenix を入れています

```Dockerfile
RUN mix local.hex --force \
  && mix archive.install hex phx_new --force \
  && mix local.rebar --force

RUN apt-get update \
  && apt-get upgrade -y \
  && apt-get install --no-install-recommends -y \
    libopencv-dev \
    build-essential \
    erlang-dev
```

しかし、 Livebook 上から evision をインストールすると10分以上ビルドに時間がかかってしまったため、
以下のようにフラグを立て、ビルド済のものを使うようにしました

```Dockerfile
ENV EVISION_PREFER_PRECOMPILED=true
```

### evision のインストール実行

他に使うものも併せてインストールします

```elixir
Mix.install([
  {:httpoison, "~> 1.8"},
  {:evision, "~> 0.1"},
  {:kino, "~> 0.8"},
  {:nx, "~> 0.4"}
])
```

## 画像生成

Nx を使ってテンソルから画像生成できます

```elixir
[
  [
    [255, 0, 0],
    [255, 128, 0],
    [255, 255, 0]
  ],
  [
    [0, 255, 0],
    [0, 255, 128],
    [0, 255, 255]
  ],
  [
    [0, 0, 255],
    [128, 0, 255],
    [255, 0, 255]
  ]
]
|> Nx.tensor(type: {:u, 8})
# Evision のマトリックスに変換
|> Evision.Mat.from_nx_2d()
# 見やすいように拡大
|> Evision.resize({300, 300}, interpolation: Evision.cv_INTER_AREA())
```

![スクリーンショット 2022-12-13 12.00.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/bd3a0d62-07c9-a451-238f-ef452790919f.png)

## 画像のダウンロード

HTTPoison を使って画像のバイナリデータを取得します

```elixir
img_binary =
  "https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png"
  |> HTTPoison.get!()
  |> then(&(&1.body))
```

![スクリーンショット 2022-12-13 12.01.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/078e1833-0179-163d-e97f-afe2c06ed6cb.png)

バイナリデータをデコードします

```elixir
img =
  img_binary
  |> Evision.imdecode(Evision.cv_IMREAD_COLOR())
```

![スクリーンショット 2022-12-13 12.02.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/2f41ed59-dca8-1d82-5600-08d7545ee53d.png)

## 画像の書込

`Evision.imwrite` で画像をファイルに保存します

```elixir
lenna_path = "Lenna.png"

Evision.imwrite(lenna_path, img)
```

## 画像の読込

Python の OpenCV と同じように画像ファイルを読み込みます

```elixir
img = Evision.imread(lenna_path)
```

![スクリーンショット 2022-12-13 12.03.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b7b44932-1eff-3e95-0102-1ab1eefc547e.png)

画像サイズも以下のようにして取得できます

```elixir
size = Evision.Mat.shape(img)
```

`Evision.Mat.to_nx` でテンソル化することもできます

```elixir
Evision.Mat.to_nx(img)
```

## リサイズ

リサイズも Python と同じように記述可能です

```elixir
Evision.resize(img, {256, 128})
```

![スクリーンショット 2022-12-13 12.05.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/664dc284-fda7-16aa-3005-03564ecfb1eb.png)

## グレースケール

グレースケールでの読込もシンプルに記述できます

```elixir
Evision.imread(lenna_path, flags: Evision.cv_IMREAD_GRAYSCALE())
```

![スクリーンショット 2022-12-13 12.06.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/bdd79011-92a1-3d31-d954-3831447cd7aa.png)

読込済のマトリックスをグレースケールに変換する場合は `Evision.cvtColor` を使います

定数は `cv_` で evision に定義されています

```elixir
Evision.cvtColor(img, Evision.cv_COLOR_BGR2GRAY())
```

## 二値化

閾値を指定しての二値化も同様です

```elixir
{threshold, mono_img} =
  lenna_path
  |> Evision.imread(flags: Evision.cv_IMREAD_GRAYSCALE())
  |> Evision.threshold(127, 255, Evision.cv_THRESH_BINARY())

IO.inspect(threshold)

mono_img
```

![スクリーンショット 2022-12-13 12.08.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/2bff370c-83c2-8682-349a-7048f1a6d586.png)

## 平行移動

平行移動にはアフィン変換を利用します

アフィン変換用の変換行列は以下のように定義します

まず2次元リストで定義した後、
それを Nx で tensor に変換し、
evision で更に matrix に変換します

```elixir
affine =
  [
    [1, 0, 100],
    [0, 1, 50]
  ]
  |> Nx.tensor(type: {:f, 32})
  |> Evision.Mat.from_nx()
```

あとは Python と同じように `wrapAffine` に変換行列と出力サイズを与えるだけです

```elixir
Evision.warpAffine(img, affine, {512, 512})
```

![スクリーンショット 2022-12-13 12.09.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/283fe238-3e9e-2425-c073-2275a554778b.png)

## 回転

回転の場合は `getRotationMatrix2D` に回転の中心座標、角度、スケールを指定して変換行列を取得します

```elixir
affine = Evision.getRotationMatrix2D({512 / 2, 512 / 2}, 70, 1)

Evision.warpAffine(img, affine, {512, 512})
```

![スクリーンショット 2022-12-13 12.09.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/536b72ee-d729-ea31-12d7-db40b06d7dc2.png)

## ぼかし

その他、各種フィルター系も Python と同じように記述できます

### 通常のブラー

```elixir
Evision.blur(img, {9, 9})
```

![スクリーンショット 2022-12-13 12.10.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/4bc4a4c3-6750-947d-9e64-c72ecd45cae0.png)

### 中央値ブラー

```elixir
Evision.medianBlur(img, 9)
```

![スクリーンショット 2022-12-13 12.10.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b01ab280-0241-87c4-a973-64aaa323f401.png)

### ガウシアンブラー

```elixir
Evision.gaussianBlur(img, {9, 9}, 5)
```

![スクリーンショット 2022-12-13 12.11.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6296bf6a-cbf6-94f2-a108-3dff3618f312.png)

## 図形描画

四角形や楕円などの図形も描画できます

### 線

```elixir
img
# 直線
|> Evision.line(
  # 始点{x, y}
  {200, 400},
  # 終点{x, y}
  {300, 450},
  # 色{R, G, B}
  {0, 255, 255},
  # 線の太さ
  thickness: 5
)
# 矢印
|> Evision.arrowedLine(
  # 始点{x, y}
  {300, 200},
  # 終点{x, y}
  {400, 150},
  # 色{R, G, B}
  {255, 255, 0},
  # 線の太さ
  thickness: 3,
  # 頭の大きさ
  tipLength: 0.3
)
```

![スクリーンショット 2022-12-13 12.12.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9ae9f748-1812-66a4-c29e-740808d41ceb.png)

### 四角形

```elixir
img
# 四角形
|> Evision.rectangle(
  # 左上{x, y}
  {150, 120},
  # 右下{x, y}
  {225, 320},
  # 色{R, G, B}
  {0, 0, 255},
  # 線の太さ
  thickness: 12,
  # 線の引き方（角がギザギザになる）
  lineType: Evision.cv_LINE_4()
)
|> Evision.rectangle(
  # 左上{x, y}
  {50, 120},
  # 右下{x, y}
  {125, 320},
  # 色{R, G, B}
  {0, 0, 255},
  # 線の太さ
  thickness: 12,
  # 線の引き方（角が滑らかになる）
  lineType: Evision.cv_LINE_AA()
)
|> Evision.rectangle(
  # 左上{x, y}
  {250, 60},
  # 右下{x, y}
  {325, 110},
  # 色{R, G, B}
  {0, 255, 0},
  # 塗りつぶし
  thickness: -1
)
```

![スクリーンショット 2022-12-13 12.12.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/47b7b997-ff44-dfe8-6f0d-31be9a26ef14.png)

### 楕円、扇形

```elixir
img
# 円
|> Evision.circle(
  # 中心{x, y}
  {100, 100},
  # 半径
  50,
  # 色{R, G, B}
  {255, 0, 0},
  # 塗りつぶし
  thickness: -1
)
# 楕円
|> Evision.ellipse(
  # 中心{x, y}
  {300, 300},
  # {長径, 短径}
  {100, 200},
  # 回転角度
  30,
  # 弧の開始角度
  0,
  # 弧の終了角度
  360,
  # 色{R, G, B}
  {255, 255, 0},
  # 線の太さ
  thickness: 3
)
# 扇形
|> Evision.ellipse(
  # 中心{x, y}
  {400, 200},
  # {長径, 短径}
  {100, 100},
  # 回転角度
  0,
  # 弧の開始角度
  100,
  # 弧の終了角度
  200,
  # 色{R, G, B}
  {0, 255, 0},
  # 塗りつぶし
  thickness: -1
)
```

![スクリーンショット 2022-12-13 12.13.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5f160c21-0a20-9984-27f2-a1734b26f3e4.png)

## 文字描画

文字も書けます

```elixir
img
|> Evision.putText(
  # 文字列
  "Lenna",
  # 左下{x, y}
  {150, 200},
  # フォント種類
  Evision.cv_FONT_HERSHEY_SIMPLEX(),
  # フォントサイズ
  2.5,
  # 文字色
  {0, 0, 255},
  # 文字太さ
  thickness: 5
)
```

![スクリーンショット 2022-12-13 12.14.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/7f55fdf1-184b-0fe7-cf18-d4652f412020.png)

## Nx との連携

OpenCV と numpy のように、 Evision と Nx も連携できます

`Evision.Mat.to_nx` と `Evision.Mat.from_nx_2d` で Nx テンソルと相互変換できます

```elixir
img
|> Evision.Mat.to_nx(Nx.BinaryBackend)
|> Nx.transpose(axes: [1, 0, 2])
|> Evision.Mat.from_nx_2d()
|> dbg
```

![スクリーンショット 2022-12-13 12.15.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/1022c425-f58a-80cd-a9a6-13fe30eea63a.png)

```elixir
img
|> Evision.Mat.to_nx(Nx.BinaryBackend)
|> Nx.mean(axes: [2])
|> Nx.broadcast({512, 512, 3}, axes: [0, 1])
|> Evision.Mat.from_nx_2d()
|> dbg
```

![スクリーンショット 2022-12-13 12.16.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9eaa9f90-ff73-c3d1-3f00-cebff94942a8.png)

## おわりに

Evision を使うことで、 Python と同じことが同じように Elixir で実装できました

Elixir で画像処理が簡単に実装できるため、バックエンドの実装の幅が広がりますね