はじめに
画像が暗くて見づらい、印象が悪く見えてしまう、そんなとき、ありますよね
その問題、解決できます
そう、 Elixir ならね
というわけでガンマ補正で画像の明るさを調整します
もちろん Elixir 、 Nx 、 Evision の組み合わせです
当然のように Livebook で実行します
実装の全量はこちら
実行環境
Livebook 0.8.0 の Docker イメージを元にしたコンテナで動かしました
コンテナ定義はこちらを参照
セットアップ
必要なモジュールをインストールします
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
内で演算子による行列演算が記述できます
import Nx.Defn
画像のダウンロード
いつものように Lenna さんを呼びます
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())
これが元画像になります
ガンマ補正
ガンマ補正モジュールを以下のように定義します
GammaCorrection.correct
に画像と gamma パラメータを渡すことで明るさ補正ができます
gamma < 1.0 のとき暗くなり、 gamma > 1.0 のとき明るくなります
中身は後で解説します
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
では実行してみましょう
元画像とガンマ補正結果を並べてみます
[
img,
GammaCorrection.correct(img, 5.0)
]
|> Kino.Layout.grid(columns: 2)
明るくなりましたね
次は 0.25 から 2.0 の範囲でガンマ補正をかけてみます
1..8
|> Enum.to_list()
|> Enum.map(fn gamma_seed -> GammaCorrection.correct(img, gamma_seed / 4) end)
|> Kino.Layout.grid(columns: 4)
左上(gamma = 0.25)から右下(gamma = 2.0)に行くにつれて、どんどん明るくなるのがわかると思います
演算内容の確認
ガンマ補正で使っているテンソルの中身を見てみましょう
gamma_tensor = GammaCorrection.generate_gamma_tensor(0.1)
うーん、これだけを見てもなんともですね
gamma を変化させながらグラフにして見ましょう
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)
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)
このグラフの 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, ...](長さ=256)
0 -> 255, 1-> 100, 2 -> 0, 3 -> 200, 4 -> 500, ... というように変換する -
dst(変換後): [255, 100, 0, 255, 100, 0]
なので、 gamma = 10 の変換テーブルを使えば小さい値(暗い色)でも大きい値(明るい値)に変換される結果、画像が明るくなります
逆に gamma = 0.1 の変換テーブルを使うと大きい値でも小さい値に変換されるため、画像が暗くなります
まとめ
思った以上にガンマ補正がシンプルに実装できました
Elixir だとやっぱり書くのが楽しいですね