Elixir OpenCV: StbImageとNx.Tensor と Evision.Mat で相互変換

Elixir で [OpenCV] を使うときにはどのような素材をどこから取ってきてどう加工するかによって求められる関数が異なり、場合により複数のパッケージが連携する必要があるかと思います。

@RyoWakabayashi さんの[Elixir Image と Nx と evision で相互変換](https://qiita.com/RyoWakabayashi/items/34de2207bcf8d745a01a) が大変参考になりました。

ここでは [elixir-image/image] よりシンプルな [elixir-nx/stb_image] と [cocoa-xu/evision] で相互変換をしてみようと思います。

https://qiita.com/RyoWakabayashi/items/34de2207bcf8d745a01a

[OpenCV]: https://ja.wikipedia.org/wiki/OpenCV
[elixir-image/image]: https://github.com/elixir-image/image
[elixir-nx/stb_image]: https://github.com/elixir-nx/stb_image
[cocoa-xu/evision]: https://github.com/cocoa-xu/evision

## セットアップ

```elixir
Mix.install([
  {:nx, "~> 0.5.0"},
  {:evision, "0.1.31"},
  {:stb_image, "~> 0.6.0"},

  # 画像を Web からダウンロードするため
  {:req, "~> 0.3.0"},

  # Livebook上で画像を表示するため
  # {:kino, "~> 0.9.0"}
])
```

[cocoa-xu/evision] は [パッチバージョンでも破壊的な変更](https://github.com/cocoa-xu/evision/blob/main/CHANGELOG.md)があるようのでバージョンをロックしておいた方が無難そうです。

https://qiita.com/RyoWakabayashi/items/2846133bc7014319d172

[cocoa-xu/evision] の作者は好んで `Cv` エイリアスを使用しているのでそれに則ります。

https://github.com/cocoa-xu/evision/tree/main/examples

```elixir
alias Evision, as: Cv
```

## 画像加工の一例

一例として画像をこのように加工することができます。ここで `StbImage` と `Nx.Tensor` と `Evision.Mat` で相互変換がされています。

```elixir
img_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/65/Antonio_Inoki_IMG_0398-2_20121224.JPG/330px-Antonio_Inoki_IMG_0398-2_20121224.JPG"

{x, y, w, h} = {40, 30, 250, 250}
rect_start_point = {x, y}
rect_end_point = {x + w, y + h}
rect_color = {0, 255, 0}
rect_options = [thickness: 5]

Req.get!(img_url)
|> then(& &1.body)
|> StbImage.read_binary!()
|> StbImage.to_nx()
|> Cv.Mat.from_nx_2d()
|> Cv.cvtColor(Cv.Constant.cv_COLOR_BGR2RGB())
|> Cv.rectangle(rect_start_point, rect_end_point, rect_color, rect_options)
```

こういう書き方もできそうです。

```elixir
img_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/65/Antonio_Inoki_IMG_0398-2_20121224.JPG/330px-Antonio_Inoki_IMG_0398-2_20121224.JPG"
save_as = Path.join(System.tmp_dir!(), URI.encode_www_form(img_url))

{x, y, w, h} = {40, 30, 250, 250}
rect_start_point = {x, y}
rect_end_point = {x + w, y + h}
rect_color = {0, 255, 0}
rect_options = [thickness: 5]

Req.get!(img_url, output: save_as)
|> then(fn _ -> Cv.imread(save_as) end)
|> Cv.rectangle(rect_start_point, rect_end_point, rect_color, rect_options)
```

![アントニオ猪木](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82804/6f2b38ec-dac3-8ca2-32ce-a2c654756a03.png)

image creadit: https://ja.wikipedia.org/wiki/アントニオ猪木

## 画像の読込

画像はバイナリとして読み込む場合もあれば、ファイルを読み込む場合もあると思います。

`Req.get!/2` を用いて Web から画像バイナリをダウンロードすることができます。

```elixir
img_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/65/Antonio_Inoki_IMG_0398-2_20121224.JPG/330px-Antonio_Inoki_IMG_0398-2_20121224.JPG"

img_data =  Req.get!(img_url) |> then(& &1.body)
```

また、画像をファイルとしてローカルに保存したい場合があるかもしれません。

```elixir
download = fn url ->
  save_as = Path.join(System.tmp_dir!(), URI.encode_www_form(url))
  Req.get!(url, output: save_as)
  save_as
end

img_file = download.(img_url)
```

## Binary -> StbImage

画像バイナリの読み込みには `StbImage.read_binary!/1` が手軽で便利です。`StbImage` 構造体が帰ります。

```elixir
img_stb = StbImage.read_binary!(img_data)
```

## File -> Evision.Mat

ローカルに保存された画像ファイルを読み込む場合は `Cv.imread/1` が使えます。

```elixir
img_mat = Cv.imread(img_file)
```

## StbImage <-> Nx.Tensor

`StbImage` は簡単に `Nx.Tensor` へ変換できます。

```elixir
img_nx = StbImage.to_nx(img_stb)
```

逆も簡単です。

```elixir
img_stb = StbImage.from_nx(img_nx)
```

## Nx.Tensor <-> Evision.Mat

`Nx.Tensor` から `Evision.Mat` への変換も基本的には同様にできるのですが、ここでは注意が必要です。2点あります。

### RGB vs BGR

- OpenCVは色チャネルをB（青）・G（緑）・R（赤）の順に保持しています。ですので、適切な場所で B と R 入れ替える必要があります。

### Evision.Mat.from_nx vs Evision.Mat.from_nx_2d

- これは正直いうとよくわかりませんが、2Dの画像を取り扱う場合は `from_nx_2d` を使わないとうまく処理ができないようです。OpenCV の関数を使って画像加工しても何も起こらない場合はここを見落としている可能性があります。エラーがでないので厄介です。

```elixir
img_nx =
  img_mat
  |> Cv.cvtColor(Cv.Constant.cv_COLOR_BGR2RGB())
  |> Cv.Mat.to_nx()

img_mat =
  img_nx
  |> Cv.Mat.from_nx_2d()
  |> Cv.cvtColor(Cv.Constant.cv_COLOR_BGR2RGB())
```

## Evision.Mat <-> StbImage

`Evision.Mat` と `StbImage` との間では直接の変換はできませんが、`Nx.Tensor` を介在させることにより実現可能です。

```elixir
img_mat
|> Cv.cvtColor(Cv.Constant.cv_COLOR_BGR2RGB())
|> Cv.Mat.to_nx()
|> StbImage.from_nx()

img_stb
|> StbImage.to_nx()
|> Cv.Mat.from_nx_2d()
|> Cv.cvtColor(Cv.Constant.cv_COLOR_BGR2RGB())
```

## アルファチャネルを取り除く変換

場合によりアルファチャネルを取り除きたい場合があると思いますが、現時点では `Evision.Backend` はスライス構文に対応していないようです。

```elixir
Cv.Mat.to_nx(img_mat)[[.., .., 0..2]]

# ** (RuntimeError) operation slice is not yet supported on Evision.Backend.
# Please use another backend like Nx.BinaryBackend or Torchx.Backend.
#   To use Torchx.Backend, :torchx should be added to your app's deps.
#   Please see <https://github.com/elixir-nx/nx/tree/main/torchx> for more information on how to install and use it.
# To convert the tensor to another backend, please use Evision.Mat.to_nx(tensor, Backend.ModuleName)
#   for example, Evision.Mat.to_nx(tensor, Nx.BinaryBackend) or Evision.Mat.to_nx(tensor, Torchx.Backend).
```

`Evison.Mat` を `Nx.Tensor` に変換するときにバックエンドをしていするとうまくいきます。

```elixir
Cv.Mat.to_nx(img_mat, Nx.BinaryBackend)[[.., .., 0..2]]
```