はじめに
バッチリメイクをしたはずなのに写真ではイマイチ印象に残らない、そんなとき、ありますよね
その問題、解決できます
そう、 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 だけです
画像のダウンロード
いつものように 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())
これが元画像になります
グレースケールの場合
まずは単純なグレースケールの場合について実装します
画像をグレースケールに変換します
gray_img = Evision.cvtColor(img, Evision.Constant.cv_COLOR_BGR2GRAY())
このグレーススケールの各ピクセルの値についてヒストグラムを作ってみます
ヒストグラムとは、値毎の件数をグラフ化したものです
画像の場合、 0 (真っ白)から 255 (真っ黒)の値がそれぞれ画像内に何回出現したか、を表します
例えば 128 (灰色)近辺の件数が高く、 0 や 255 近辺の件数が少ない = ヒストグラムが偏っている場合、画像全体が同じような灰色になっている、ということなので、コントラストが弱いと言えます
逆に 0 から 255 まで満遍なく件数が同じくらいある = ヒストグラムが平坦になっている場合、真っ白な部分も真っ黒な部分も灰色の部分も同じくらいある、ということになるのでコントラストが高いと言えます
pixel =
gray_img
|> Evision.Mat.to_nx()
|> Nx.to_flat_list()
VegaLite.new()
|> VegaLite.data_from_values(x: pixel)
|> VegaLite.mark(:bar)
|> VegaLite.encode_field(:x, "x", type: :quantitative)
|> VegaLite.encode_field(:y, "x", type: :quantitative, aggregate: :count)
ヒストグラムを見ると、 50 あたりと 150 あたりの値が多いことが分かります
そして 0 や 255 の近辺は明らかに少ないです
では、ヒストグラムを平坦にしてみましょう
Evision.equalizeHist に画像を渡すだけです
equalized_img = Evision.equalizeHist(gray_img)
画像がくっきりしたように見えます
元画像と並べてみましょう
Kino.Layout.grid([gray_img, equalized_img], columns: 2)
明らかにヒストグラム平坦化した方がくっきり、コントラストが強くなっていますね
平坦化した画像のヒストグラムを確認してみましょう
equalized_pixel =
equalized_img
|> Evision.Mat.to_nx()
|> Nx.to_flat_list()
VegaLite.new()
|> VegaLite.data_from_values(x: equalized_pixel)
|> VegaLite.mark(:bar)
|> VegaLite.encode_field(:x, "x", type: :quantitative)
|> VegaLite.encode_field(:y, "x", type: :quantitative, aggregate: :count)
完全な平坦ではないですが、元と比較すると凹凸が小さくなっています
カラーの場合
カラーの場合は少し難しくなります
Evision.equalizeHist はグレースケール(チャネル数=色数1)にしか使えません
かといって R G B それぞれについてヒストグラムを平坦化すると、色味が変わってしまいます
なので、画像を BGR 色空間から HSV 色空間に変換します
BGR は青(Blue)、緑(Green)、赤(Red)の頭文字で、それぞれの原色がどれくらい強いかで色を表現します
HSV は色相(Hue)、彩度(Saturation)、明度(Value)の頭文字で、色の種類、鮮やかさ、明るさの組み合わせで色を表現します
色の種類や鮮やかさは変えずに明るさだけ変えたいので、 HSV から V だけ取り出してヒストグラム平坦化してから、 HS とくっ付ければいいわけです
hsv_img = Evision.cvtColor(img, Evision.Constant.cv_COLOR_BGR2HSV())
HSV に変換しても画像を表示するときには BGR として表示しようとするため、なんだかミステリアスな画像になっています
HSV から V を取り出すので、 Nx の行列にしてから 2 次元目の 2 列目を抜き出します
この値は明るさだけを取り出しているので、グレースケール画像のように表示できます
Evision のマトリックスを Evision.Mat.to_nx で Nx のテンソルに変換しています
Evision.Backend だと対応していない関数があるため、 Nx.BinaryBackend を指定しています
value =
hsv_img
|> Evision.Mat.to_nx(Nx.BinaryBackend)
|> Nx.slice_along_axis(2, 1, axis: 2)
Kino.Image.new(value)
この V に対して Evision.equalizeHist を実行します
少し気をつけないといけないのは、次元数を下げてから実行し、その後に戻すことです
equalized_value =
value
# 次元数を下げる {縦, 横, 1} -> {縦, 横}
|> Nx.squeeze()
|> Evision.equalizeHist()
|> Evision.Mat.to_nx(Nx.BinaryBackend)
# 次元数を上げる {縦, 横} -> {縦, 横, 1}
|> Nx.new_axis(2)
Kino.Image.new(equalized_value)
この時点でコントラストが強調されたのが分かります
最後に HS と V を結合します
hs =
hsv_img
|> Evision.Mat.to_nx(Nx.BinaryBackend)
|> Nx.slice_along_axis(0, 2, axis: 2)
equalized_img =
[hs, equalized_value]
|> Nx.concatenate(axis: 2)
|> Evision.Mat.from_nx_2d()
|> Evision.cvtColor(Evision.Constant.cv_COLOR_HSV2BGR())
カラーでもくっきりしています
元画像と並べて見ましょう
Kino.Layout.grid([img, equalized_img], columns: 2)
明確にコントラストが強調されていますね
まとめ
VegaLite だとヒストグラムも簡単に作れるので、平坦化の概念が理解しやすかったです
参考サイト










