Elixir Image （Vix, libvips）で簡単画像処理

## はじめに

Elixir の Image という、そのまんまのネーミングのモジュールを使って画像処理を行います

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

Image は Vix という画像処理モジュールをより使いやすくしたモジュールです

https://github.com/akash-akya/vix

そして Vix は libvips という軽量、高速な画像処理ライブラリの Elixir ラッパーです

https://www.libvips.org/

今回も Livebook を使っていきます

https://livebook.dev/

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/vix/image.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/

:::

### 実行環境の注意点

Image (Vix) を使うためには libvpc が必要です

Ubuntu　の場合は `apt-get install libvpc-dev` を実行してください

## 事前準備

適当な画像を以下のパスに用意したものとします

- /tmp/puppies.jpg （子犬画像、なんでも良い）
- /tmp/noize.png (黒背景に白で単純な模様を描いた画像、点々のノイズがある)

私が用意したのはこちらの画像

![puppies.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f8584e85-4085-75d8-9be0-cfe3c50c00c5.jpeg)

![noize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/874b73f1-6646-c9a5-0730-061e62978263.png)

また、クロマキー用に背景が一色の画像も任意の場所に用意しておいてください

私が用意したのはこちらの画像

![pen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f93cf3e9-2f58-1dd4-edcd-c2e0c52691fa.png)

## セットアップ

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

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

## 画像の読込

### ファイルから

`Image.open!` にパスを渡すと画像を開きます

```elixir
puppies_img = Image.open!("/tmp/puppies.jpg")
```

結果は以下のように、属性情報と画像のタブ表示になります

![image_tabs.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e27e2945-4459-be78-8bb5-31efbed555bd.gif)

### Kino.Input から

Kino.Input からの画像を読み込みます

以下を実行して表示される画像アップロードに、事前準備していたクロマキー用の画像を入力してください

```elixir
chroma_key_img_input = Kino.Input.image("CHROMA KEY IMAGE", format: :png)
```

`Image.from_binary` でバイナリを読み込みます

```elixir
chroma_key_img =
  chroma_key_img_input
  |> Kino.Input.read()
  |> then(& &1.data)
  |> Image.from_binary!()

Image.Kino.show(chroma_key_img, max_height: 400)
```

![pen_r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ce516d36-2bff-9d2f-4b71-2ed2e704579f.png)

### Web から

Web からダウンロードする場合も同様です

```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.Kino.show(lenna_img, max_height: 400)
```

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

## 画像処理

色々な画像処理を実行してみましょう

### 色変換

`Image.to_colorspace!` で色を変換します

以下のコードはカラー画像を白黒にします

```elixir
puppies_img
|> Image.to_colorspace!(:bw)
|> Image.Kino.show()
```

![puppies_bw.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/dad27cb8-7c1c-ec0e-994f-be54caf8020d.png)

### ぼかし

`Image.blur!` で画像をぼかします

```elixir
puppies_img
|> Image.blur!()
|> Image.Kino.show()
```

![blur.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/c17c5d1b-20ec-585c-442f-e801dd8ac94e.png)

sigma に 1.0 から 10.0 の値を与えることでぼかし具合を調整します

```elixir
1..10
|> Enum.map(fn sigma ->
  puppies_img
  |> Image.blur!(sigma: sigma)
  |> Image.Kino.show()
end)
|> Kino.Layout.grid(columns: 5)
```

![blur_sigma.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5d0637d5-0f71-0f66-e801-86261e4d07b4.png)

### モザイク

`Image.pixelate!` でモザイクを掛けます

```elixir
puppies_img
|> Image.pixelate!()
|> Image.Kino.show()
```

![pixelated.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/c8257568-0319-aeb8-429b-76c073002177.png)

第2引数にモザイクの大きさ（画像全体を 1.0 としたときの相対値）を指定してモザイクの強さを調整します

```elixir
1..10
|> Enum.map(fn scale ->
  puppies_img
  |> Image.pixelate!(scale / 100)
  |> Image.Kino.show()
end)
|> Kino.Layout.grid(columns: 5)
```

![pixelate_scale.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b209592b-1f4e-f963-2115-6d8da066fcba.png)

### リサイズ

`Image.resize!` で元の大きさを 1.0 としたときの比を指定することでリサイズできます

```elixir
puppies_img
|> Image.resize!(0.2)
|> Image.Kino.show()
```

![small.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9b2099d0-cded-0996-e139-5e02206003eb.png)

### 反転

`Image.flip!` で反転します

第2引数で方向を指定します

`:horizontal` で左右反転

```elixir
puppies_img
|> Image.flip!(:horizontal)
|> Image.Kino.show()
```

![flipped_h.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/4709d22a-6962-3325-73b4-74c7ad69415c.png)

`:vertical` で上下反転

```elixir
puppies_img
|> Image.flip!(:vertical)
|> Image.Kino.show()
```

![flipped_v.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e9199756-0cfe-4256-3925-b9775177f22e.png)

### 回転

`Image.rotate!` に角度を指定して回転します

```elixir
puppies_img
|> Image.rotate!(45)
|> Image.Kino.show()
```

![rotated.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/0f95fb7a-02da-f84b-7bc7-592794580235.png)

回転したとき余白を追加しています

せっかくなのでアニメーションで見てみましょう

```elixir
Stream.interval(1)
|> Stream.take(361)
|> Kino.animate(fn angle ->
  puppies_img
  |> Image.rotate!(angle)
  |> Image.Kino.show()
end)
```

![rotate.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/3557280b-c119-71aa-a250-64d914dd7c74.gif)

### 切り抜き

画像の特定の範囲を切り抜きます

引数は画像全体の幅、高さを 1.0 としたときの比で与えます

- 左上の X 座標
- 左上の Y 座標
- 幅
- 高さ

```elixir
puppies_img
|> Image.crop!(0.3, 0.2, 0.5, 0.7)
|> Image.Kino.show()
```

![croped.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/32473bea-a85b-67da-3a30-df87917279ee.png)

### 合成

`Image.compose!` で複数の画像を合成します

`blend_mode` で合成方法を指定します

`:over` は単純に上書きします

```elixir
puppies_img
|> Image.compose!(lenna_img, blend_mode: :over)
|> Image.Kino.show()
```

![over.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/119fa129-73e5-5a59-199f-d907c30d6d71.png)

`:lighten` は明るい方の値にします

```elixir
puppies_img
|> Image.compose!(lenna_img, blend_mode: :lighten)
|> Image.Kino.show()
```

![lighten.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/3b8e0857-aa22-f265-6203-a50147e7fc61.png)

`:add` は画素値を足します

```elixir
puppies_img
|> Image.compose!(lenna_img, blend_mode: :add)
|> Image.Kino.show()
```

![add.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/dd24c35e-f6f3-1224-376e-744811472ddc.png)

`:difference` は画素値を引きます（差分抽出）

```elixir
puppies_img
|> Image.compose!(lenna_img, blend_mode: :difference)
|> Image.Kino.show()
```

![difference.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/4b18007a-56a8-6f7a-c903-e1fb607d67f5.png)

### 膨張、収縮(ノイズ除去)

ノイズが乗っている画像を読み込みます

```elixir
noize_img = Image.open!("/tmp/noize.png")

Image.Kino.show(noize_img)
```

![noize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/874b73f1-6646-c9a5-0730-061e62978263.png)

画像内の白い部分を `Image.dilate!` で膨張させ、 `Image.erode!` で収縮させます

```elixir
[
  noize_img |> Image.dilate!(3),
  noize_img |> Image.erode!(3)
]
|> Enum.map(&Image.Kino.show(&1, max_height: 400))
|> Kino.Layout.grid(columns: 2)
```

![dilate_erode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9ee9396f-446e-ee36-3dff-21c990871de1.png)

収縮してから膨張させることで、ノイズ（白い点々）を除去することができます

```elixir
[
  noize_img,
  noize_img |> Image.erode!(3) |> Image.dilate!(3)
]
|> Enum.map(&Image.Kino.show(&1, max_height: 400))
|> Kino.Layout.grid(columns: 2)
```

![remove_noize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/dd518df1-11f3-8ef7-1362-ca2f340a5300.png)

### クロマキー

画像から背景を消します

`Image.chroma_color` で背景色を取得します

```elixir
chroma_key_img
|> Image.chroma_color()
|> then(&Image.new!(200, 200, color: &1, bands: 3))
|> Image.Kino.show()
```

![bg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/4c0a6f0b-5762-e0cb-3851-9ad0dc1eb016.png)

`Image.split_alpha` で画像を RGB と A　に分割します

```elixir
{rgb, alpha} = Image.split_alpha(chroma_key_img)
```

RGB 画像を `Image.chroma_mask!` に渡すと背景が黒、前景が白の画像（マスク画像）を取得できます

```elixir
[
  chroma_key_img,
  Image.chroma_mask!(rgb)
]
|> Enum.map(&Image.Kino.show(&1, max_height: 400))
|> Kino.Layout.grid(columns: 2)
```

![chroma_mask.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/d1286789-8bfd-327a-577b-a8217a5cb593.png)

RGB 画像を `Image.chroma_key!` に渡すと背景を消した画像を取得できます

```elixir
[
  chroma_key_img,
  Image.chroma_key!(rgb)
]
|> Enum.map(&Image.Kino.show(&1, max_height: 400))
|> Kino.Layout.grid(columns: 2)
```

![chroma_key.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/508b179d-c211-946f-38bc-4727fac7a925.png)

## まとめ

Image を使うことで簡単に画像処理を実装できました

まだまだ様々な機能があるので、色々試してみたいと思います