Elixir Evision と Nx で画像のコントラスト強調（ヒストグラム平坦化）を実装する

## はじめに

バッチリメイクをしたはずなのに写真ではイマイチ印象に残らない、そんなとき、ありますよね

![スクリーンショット 2022-12-06 17.36.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/22da5297-aab1-9b24-a73d-30adf96d506c.png)

その問題、解決できます

そう、 Elixir ならね

というわけでヒストグラム平均化で画像のコントラストを強調します

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

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

実装の全量はこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/evision/histogram_flatten.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　だけです

## 画像のダウンロード

いつものように 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-06 17.36.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/22da5297-aab1-9b24-a73d-30adf96d506c.png)

これが元画像になります

## グレースケールの場合

まずは単純なグレースケールの場合について実装します

画像をグレースケールに変換します

```elixir
gray_img = Evision.cvtColor(img, Evision.Constant.cv_COLOR_BGR2GRAY())
```

![スクリーンショット 2023-01-01 10.59.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ba2ba24d-609a-21cb-5b75-0129b5499acc.png)

このグレーススケールの各ピクセルの値についてヒストグラムを作ってみます

ヒストグラムとは、値毎の件数をグラフ化したものです

画像の場合、 0 （真っ白）から 255 （真っ黒）の値がそれぞれ画像内に何回出現したか、を表します

例えば 128 （灰色）近辺の件数が高く、 0 や 255 近辺の件数が少ない = ヒストグラムが偏っている場合、画像全体が同じような灰色になっている、ということなので、コントラストが弱いと言えます

逆に 0 から 255 まで満遍なく件数が同じくらいある = ヒストグラムが平坦になっている場合、真っ白な部分も真っ黒な部分も灰色の部分も同じくらいある、ということになるのでコントラストが高いと言えます

```elixir
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)
```

![gray_hist.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/253afbd5-0c1d-79ff-5081-37a4262a2d91.png)

ヒストグラムを見ると、 50 あたりと 150 あたりの値が多いことが分かります

そして 0 や 255 の近辺は明らかに少ないです

では、ヒストグラムを平坦にしてみましょう

`Evision.equalizeHist` に画像を渡すだけです

```elixir
equalized_img = Evision.equalizeHist(gray_img)
```

![スクリーンショット 2023-01-01 11.01.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/faac7e0e-da1a-91c9-f0a1-9fc5bf55fae8.png)

画像がくっきりしたように見えます

元画像と並べてみましょう

```elixir
Kino.Layout.grid([gray_img, equalized_img], columns: 2)
```

![スクリーンショット 2023-01-01 11.02.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/c7dbebb3-c414-7622-8aad-852b962d657c.png)

明らかにヒストグラム平坦化した方がくっきり、コントラストが強くなっていますね

平坦化した画像のヒストグラムを確認してみましょう

```elixir
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)
```

![ equalized_gray_hist.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/296f9887-f781-4b15-1e2c-f0abc99db2df.png)

完全な平坦ではないですが、元と比較すると凹凸が小さくなっています

## カラーの場合

カラーの場合は少し難しくなります

`Evision.equalizeHist` はグレースケール（チャネル数=色数1）にしか使えません

かといって R G B それぞれについてヒストグラムを平坦化すると、色味が変わってしまいます

なので、画像を BGR 色空間から HSV 色空間に変換します

:::note info
Evison、OpenCV では RGB ではなく BGR を使っています

たまたま最近そのことについて記事を書いている方がいました

[OpenCV のピクセル形式が BGR である理由](https://qiita.com/yoya/items/bfef7404ded22649a2af)
:::

BGR は青(Blue)、緑(Green)、赤(Red)の頭文字で、それぞれの原色がどれくらい強いかで色を表現します

HSV は色相(Hue)、彩度(Saturation)、明度(Value)の頭文字で、色の種類、鮮やかさ、明るさの組み合わせで色を表現します

色の種類や鮮やかさは変えずに明るさだけ変えたいので、 HSV から V だけ取り出してヒストグラム平坦化してから、 HS とくっ付ければいいわけです

```elixir
hsv_img = Evision.cvtColor(img, Evision.Constant.cv_COLOR_BGR2HSV())
```

![スクリーンショット 2023-01-01 11.06.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/3085c620-a09f-b5c1-5ce1-e93aeb2e5c75.png)

HSV に変換しても画像を表示するときには BGR として表示しようとするため、なんだかミステリアスな画像になっています

HSV から V を取り出すので、 Nx の行列にしてから 2　次元目の 2 列目を抜き出します

この値は明るさだけを取り出しているので、グレースケール画像のように表示できます

:::note info
Evision のマトリックスを `Evision.Mat.to_nx` で Nx のテンソルに変換しています

Evision.Backend だと対応していない関数があるため、 Nx.BinaryBackend を指定しています
:::

```elixir
value =
  hsv_img
  |> Evision.Mat.to_nx(Nx.BinaryBackend)
  |> Nx.slice_along_axis(2, 1, axis: 2)

Kino.Image.new(value)
```

![スクリーンショット 2023-01-01 11.08.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/87c7fc69-611d-0616-0aa3-96c0e38ca4d0.png)

この V に対して `Evision.equalizeHist` を実行します

少し気をつけないといけないのは、次元数を下げてから実行し、その後に戻すことです

```elixir
equalized_value =
  value
  # 次元数を下げる {縦, 横, 1} -> {縦, 横} 
  |> Nx.squeeze()
  |> Evision.equalizeHist()
  |> Evision.Mat.to_nx(Nx.BinaryBackend)
  # 次元数を上げる {縦, 横} -> {縦, 横, 1} 
  |> Nx.new_axis(2)

Kino.Image.new(equalized_value)
```

![スクリーンショット 2023-01-01 11.14.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/4e0bf3e0-5ccc-c9c3-cd61-38e9637061f2.png)

この時点でコントラストが強調されたのが分かります

最後に HS と V を結合します

```elixir
hs =
  hsv_img
  |> Evision.Mat.to_nx(Nx.BinaryBackend)
  |> Nx.slice_along_axis(0, 2, axis: 2)
```

```elixir
equalized_img =
  [hs, equalized_value]
  |> Nx.concatenate(axis: 2)
  |> Evision.Mat.from_nx_2d()
  |> Evision.cvtColor(Evision.Constant.cv_COLOR_HSV2BGR())
```

![スクリーンショット 2023-01-01 11.15.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b40bc98c-9079-74cf-d817-fc66e6ed3cd6.png)

カラーでもくっきりしています

元画像と並べて見ましょう

```elixir
Kino.Layout.grid([img, equalized_img], columns: 2)
```

![スクリーンショット 2023-01-01 11.16.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/98e34afa-31e6-15c7-d4d7-7ee83aea18c4.png)

明確にコントラストが強調されていますね

## まとめ

VegaLite だとヒストグラムも簡単に作れるので、平坦化の概念が理解しやすかったです

## 参考サイト

https://kp-ft.com/717