Elixir Image と Nx と evision で相互変換

## はじめに

Elixir の Image モジュールは軽量、高速な画像処理モジュールです

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

Elixir には他にも画像を扱うことのできるモジュールとして、 Nx と evision があります

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

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

Image は libvips というライブラリを利用していて、 `Vix.Vips.Image` という構造体で画像を扱います

Nx は画像を横×縦×チャネル（RGBなどの色空間）のテンソル `Nx.Tensor` として扱います

evision は OpenCV のラッパーになっていて、画像は行列（マトリックス） `Evision.Mat` として扱います

Image モジュールにはこれらの相互変換用関数が用意されているため、これら全ては連携可能です

いつものように Livebook を使います

https://livebook.dev/

実装したノートブックはコチラ

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/vix/conversion.livemd

## 実行環境

- Elixir: 1.14.2 OTP 24
- Livebook: 0.9.3

以下のリポジトリーの Docker コンテナ上で起動しています

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

:::note info

Docker が使える環境であれば簡単に実行できます

https://docs.docker.com/engine/install/

Docker Desktop を無償利用できない場合は Rancher Desktop を使ってください

https://rancherdesktop.io/

:::

## セットアップ

必要なモジュールをインストールします

```elixir
Mix.install([
  {:image, "~> 0.35"},
  {:nx, "~> 0.5"},
  {:evision, "~> 0.1"},
  {:req, "~> 0.3"},
  {:kino, "~> 0.9"}
])
```

Req は画像を Web からダウンロードするのに使っています

https://github.com/wojtekmach/req

## 画像の読込

毎度の如くレナさんの画像をダウンロードしてきます

まず最初は `Image.from_binary` で読み込み、 `Vix.Vips.Image` の形にしておきます

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

![image.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/c74742e4-fae5-65a2-539b-71a0c72e739b.gif)

`Vix.Vips.Image` の場合、画像自体と属性情報のタブが表示されます

## Image から Nx

`Image.to_nx` で `Vix.Vips.Image` から `Nx.Tensor` に変換します

```elixir
{:ok, nx_lenna_img} = Image.to_nx(lenna_img)

nx_lenna_img
```

実行結果は以下のようにテンソルになっています

```elixir
#Nx.Tensor<
  u8[width: 512][height: 512][bands: 3]
  [
    [
      [226, 137, 125],
      [226, 137, 125],
      ...
    ],
    ...
  ]
>
```

一応、テンソルを画像として表示します（画像上は何も変わりません）

```elixir
lenna_img
|> Image.to_nx!()
|> Kino.Image.new()
```

![lenna](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/0b1aae1e-196f-a3db-bc57-6ebcc41ffc84.png)

## Nx から Image

`Image.from_nx` で `Nx.Tensor` から `Vix.Vips.Image` に変換します

```elixir
{:ok, image_lenna_img} = Image.from_nx(nx_lenna_img)

image_lenna_img
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ef559215-95a7-b088-85fb-fdd76069f1d8.png)

## Image から evision

`Image.to_evision` で `Vix.Vips.Image` から `Evision.Mat` に変換します

```elixir
{:ok, evision_lenna_img} = Image.to_evision(lenna_img)

evision_lenna_img
```

![evision.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/d5f4b3f5-4d6c-4dbb-68f5-b5116ee320e0.gif)

`Evision.Mat` の場合は画像自体、行列としての情報、テンソルとしての情報をタブ表示します

## evision から Image

`Image.from_evision` で `Evision.Mat` から `Vix.Vips.Image` に変換します

```elixir
{:ok, image_lenna_img} = Image.from_evision(evision_lenna_img)

image_lenna_img
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ef559215-95a7-b088-85fb-fdd76069f1d8.png)

## 組み合わせ処理

単に変換するだけだと画像としては何も変わらないので、 evision と Image の組み合わせ処理を実装してみます

Image で四角形を描いた後、 evision で台形変換をしてみます

```elixir
input =
  Nx.tensor(
    [
      [0, 0],
      [0, 512],
      [512, 0],
      [512, 512]
    ],
    type: :f32
  )

output =
  Nx.tensor(
    [
      [192, 256],
      [0, 512],
      [320, 256],
      [512, 512]
    ],
    type: :f32
  )

perspective_mat = Evision.getPerspectiveTransform(input, output)

lenna_img
|> Image.Draw.rect!(190, 200, 180, 200, fill: false, color: :red, stroke_width: 24)
|> Image.to_evision()
|> elem(1)
|> Evision.warpPerspective(perspective_mat, {512, 512})
|> Image.from_evision()
|> elem(1)
```

![lenna_daikei.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/713363fb-f1ad-05e2-338f-45704a3d1552.png)

レナさんが何だか強そうになりました

## まとめ

Image も evision もそれぞれ多様な関数を持っているので、組み合わせれば大概のことは実装できますね