Elixir Evision と Nx で画像の明るさ調整（ガンマ補正）を実装する

## はじめに

画像が暗くて見づらい、印象が悪く見えてしまう、そんなとき、ありますよね

![dark.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ee2de8a5-4003-615c-fc97-08e61504ddf6.png)

その問題、解決できます

そう、 Elixir ならね

というわけでガンマ補正で画像の明るさを調整します

もちろん Elixir 、 Nx 、 Evision の組み合わせです

当然のように Livebook で実行します

実装の全量はこちら

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

## 実行環境

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

## セットアップ

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

```elixir
Mix.install([
  {:req, "~> 0.3"},
  {:evision, "~> 0.1"},
  {:kino, "~> 0.8"},
  {:nx, "~> 0.4"},
  {:kino_vega_lite, "~> 0.1.7"}
])
```

- Req: 元画像のダウンロード
- Evision: 画像処理
- Kino: 結果表示
- Nx: 行列演算
- Kino_vega_lite: グラフ表示

実行自体に必要なのは Nx と Evision　だけです

行列演算を見やすくしたいので Nx.Defn をインポートしておきます

これで `defn` 内で演算子による行列演算が記述できます

```elixir
import Nx.Defn
```

## 画像のダウンロード

いつものように Lenna さんを呼びます

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

これが元画像になります

![スクリーンショット 2022-12-28 11.23.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/50c6d134-f791-f8d9-4685-2343c53b35c3.png)

## ガンマ補正

ガンマ補正モジュールを以下のように定義します

`GammaCorrection.correct` に画像と gamma パラメータを渡すことで明るさ補正ができます

gamma < 1.0 のとき暗くなり、 gamma > 1.0 のとき明るくなります

中身は後で解説します

```elixir
defmodule GammaCorrection do
  defn generate_gamma_tensor(gamma) do
    (Nx.iota({256, 1}) / 255) ** (1 / gamma) * 255
  end

  def correct(img, gamma) do
    gamma_tensor = generate_gamma_tensor(gamma)
    Evision.lut(img, gamma_tensor)
  end
end
```

では実行してみましょう

元画像とガンマ補正結果を並べてみます

```elixir
[
  img,
  GammaCorrection.correct(img, 5.0)
]
|> Kino.Layout.grid(columns: 2)
```

![スクリーンショット 2022-12-28 11.34.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/38f1b072-3dee-4af4-2c7f-6d9b603aa496.png)

明るくなりましたね

次は 0.25 から 2.0 の範囲でガンマ補正をかけてみます

```elixir
1..8
|> Enum.to_list()
|> Enum.map(fn gamma_seed -> GammaCorrection.correct(img, gamma_seed / 4) end)
|> Kino.Layout.grid(columns: 4)
```

![スクリーンショット 2022-12-28 11.27.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/a303ae15-f528-c4e4-0cf0-fcadc1082e83.png)

左上（gamma = 0.25）から右下（gamma = 2.0）に行くにつれて、どんどん明るくなるのがわかると思います

## 演算内容の確認

ガンマ補正で使っているテンソルの中身を見てみましょう

```elixir
gamma_tensor = GammaCorrection.generate_gamma_tensor(0.1)
```

![スクリーンショット 2022-12-28 11.36.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b867f7c5-08b3-6460-3a24-81545eeb111e.png)

うーん、これだけを見てもなんともですね

gamma を変化させながらグラフにして見ましょう

```elixir
data =
  [0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0]
  |> Enum.reduce(%{gamma: [], src: [], dst: []}, fn gamma, acc ->
    %{
      gamma: acc.gamma ++ List.duplicate(gamma, 256),
      src: acc.src ++ Enum.to_list(0..255),
      dst: acc.dst ++ (gamma |> GammaCorrection.generate_gamma_tensor() |> Nx.to_flat_list())
    }
  end)
```

```elixir
VegaLite.new()
|> VegaLite.data_from_values(data, only: ["src", "dst", "gamma"])
|> VegaLite.mark(:line)
|> VegaLite.encode_field(:x, "src", type: :quantitative)
|> VegaLite.encode_field(:y, "dst", type: :quantitative)
|> VegaLite.encode_field(:color, "gamma", type: :nominal)
```

![visualization.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b8159740-d9bc-7f7f-36e3-5ad849957d21.png)

このグラフの src が元の明るさ、 dst が変換後の明るさと捉えてください

gamma = 10 のグラフを見ると、 src が小さい値のときから dst の値がかなり大きくなっています
gamma = 1 のときは src = dst です
gamma = 0.1 のとき、 src が 150 を越えないとほとんど dst が大きくなりません

`Evision.lut` は Look Up Table という関数で、テーブルを元に値を変換します

変換例

- src（変換元）: [0, 1, 2, 0, 1, 2]

- 変換テーブル: [255, 100, 0, 200, 50, ...]（長さ=２５６）
  0 -> 255, 1-> 100, 2 -> 0, 3 -> 200, 4 -> 500, ... というように変換する

- dst（変換後）: [255, 100, 0, 255, 100, 0]

なので、 gamma = 10 の変換テーブルを使えば小さい値（暗い色）でも大きい値（明るい値）に変換される結果、画像が明るくなります

逆に gamma = 0.1 の変換テーブルを使うと大きい値でも小さい値に変換されるため、画像が暗くなります

## まとめ

思った以上にガンマ補正がシンプルに実装できました

Elixir だとやっぱり書くのが楽しいですね