Elixir Livebook と Python Jupyter で画像・グラフの並べ方を比較する

## はじめに

Elixir Livebook は、ブラウザ上でコードを実行して結果を表示するツールです

https://livebook.dev/

![スクリーンショット 2023-01-04 14.58.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5bd8b69a-e158-0943-8d42-9706892fecfb.png)

画像もグラフも表も地図も簡単に表示できます

![ノートブックで画像表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/faac7e0e-da1a-91c9-f0a1-9fc5bf55fae8.png)

![ノートブックでグラフ表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/296f9887-f781-4b15-1e2c-f0abc99db2df.png)

![ノートブックで表表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8e316e3b-1458-44bc-31fd-73af43a3cd9a.gif)

![ノートブックで地図表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b43ab903-4a70-66f0-4bc5-eb019680f7a6.gif)

ちなみに別の言語も実行できます

https://qiita.com/torifukukaiou/items/5b77f894aa4dc44e359d

AI・機械学習にも使えます

https://qiita.com/piacerex/items/0871b54b41128d426da5

そして Elixir は API も Web サイトもデスクトップアプリ(Windows、macOS、Linux)もモバイルアプリ(iOS、Android)も実装できるため、 Livebook でやったことは何にでも組み込めます

https://www.phoenixframework.org/

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

要するに Elixir の Livebook は　Python の Jupyter です

https://jupyter.org/

今回は画像・グラフの並べ方を例として、両者を比較してみます

例としてガンマ補正の画像表示、グラフ表示を実装してみます

## 参考サイト

Python のガンマ補正実装はこちらのサイトを参考にしました

https://pystyle.info/opencv-change-contrast-and-brightness/

他のサイトでは for 文を使って変換テーブルを生成していることが多いのですが、 for ループは非常に遅いので、せっかく numpy を使うのであれば、ちゃんと `numpy.arange` を使うこちらの実装が良いと思います

## Python Jupyter で画像・グラフを並べる

### 実行環境

- Python 3.10.9
- JupyterLab 3.5.2

### セットアップ

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

Jupyter の場合、各環境にインストール済のモジュールをそのまま使います

もし Jupyter 上からインストールする場合は `!` に続けてシェルコマンドが実行できます

```python
# !pip install opencv-python numpy matplotlib

import cv2
import numpy as np
import math
import requests
from matplotlib import pyplot as plt
```

- cv2: OpenCV 画像処理
- numpy: 行列演算
- math: 数学系ユーティリティー
- requests: HTTP リクエスト
- matplotlib: グラフ、画像出力

### 画像の取得

画像を Web から取得してきます

ストレージへの無駄なI/Oを行わないため、ファイルとして保存しないまま表示します

画像を表示する場合 `pyplot.imshow` を使用します

```python
url = "https://github.com/opencv/opencv/blob/master/samples/data/butterfly.jpg?raw=true"

# URL を指定して画像のバイナリデータをダウンロード
response = requests.get(url)

# バイナリデータを Numpy 行列に変換
array = np.frombuffer(response.content, dtype=np.uint8)

# Numpy 行列を OpenCV で画像としてデコード
org_img = cv2.imdecode(array, cv2.IMREAD_UNCHANGED)

# 画像を表示
plt.imshow(org_img)
```

![スクリーンショット 2023-01-08 19.07.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/2c4e842c-3f6f-1dd2-0141-4e2f6afe98a8.png)

### ガンマ補正

後でガンマ補正用テーブルのグラフを表示するため、テーブル生成を別関数にしています

`numpy.arange` で 0 から 255 の等差数列を生成し、ガンマ補正の計算を実行します

`cv2.LUT` で変換テーブルに従って 0 から 255 の値を変換します

```python
# ガンマ補正用変換テーブルの生成
def gamma_table(gamma):
    table = (np.arange(256) / 255) ** (1 / gamma) * 255
    # 整数に変換
    table = np.clip(table, 0, 255).astype(np.uint8)
    return table

# ガンマ補正
def gamma_correction(img, gamma):
    table = gamma_table(gamma)
    return cv2.LUT(img, table)
```

```python
# ガンマ補正を実行
corrected_img = gamma_correction(org_img, 2.0)

# ガンマ補正結果を表示
plt.imshow(corrected_img)
```

![スクリーンショット 2023-01-08 19.10.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8c2d3d38-de52-3787-d287-5109b260b9ca.png)

### 元画像と補正後画像を並べて表示

複数の画像やグラフを並べて表示する場合、 `pyplot.subplot` で位置を指定します

- 第1引数: 行数（縦に何分割するか）
- 第2引数: 列数（横に何分割するか）
- 第3引数: 左上からZ字に進むとき何番目か

```python
# 元画像
plt.subplot(1, 2, 1) # 1行２列の表示域の1番目
plt.imshow(org_img)

# 補正後画像
plt.subplot(1, 2, 2) # 1行２列の表示域の2番目
plt.imshow(corrected_img)
```

![スクリーンショット 2023-01-08 19.13.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/d0efb3df-b043-29da-f192-ab4d4ffd8477.png)

ちなみに、省略した書き方も可能です

第1引数 * 100 + 第2引数 * 10 + 第3引数 を第1引数に渡すことで同じ結果になります

```python
# 元画像
plt.subplot(121)
plt.imshow(org_img)

# 補正後画像
plt.subplot(122)
plt.imshow(corrected_img)
```

簡略に書ける一方、可読性が高いとは言えないでしょう

### 値による画像の変化を表示

Python では `for x in array:` 　で配列の要素をループできます

```python
# ガンマの値を配列で指定
gamma_array = [0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0]
```

```python
for index, gamma in enumerate(gamma_array):
    # 行数は配列の長さから計算する
    plt.subplot(math.ceil(len(gamma_array) / 4), 4, index + 1)
    plt.imshow(gamma_correction(org_img, gamma))
```

![スクリーンショット 2023-01-08 19.18.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/756d880e-d605-f0cf-4748-c652428e0396.png)

### グラフ表示

グラフを表示する場合、 `pyplot.plot` に Numpy 配列を渡すだけでいい感じに解釈して線グラフとして表示してくれます

```python
table = gamma_table(2.0)
plt.plot(table)
```

![スクリーンショット 2023-01-08 19.19.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/35c7b000-7f82-e437-9d0f-e88061763404.png)

### 値によるグラフの変化を表示

グラフも画像と同様に `subplot` を使えば並べてくれます

ただし、 `subplots_adjust` で間隔を調整しないとグラフ同士が重なってしまうことがあります

```python
# グラフ間の横間隔を調整する
plt.subplots_adjust(wspace=0.5)

for index, gamma in enumerate(gamma_array):
    plt.subplot(math.ceil(len(gamma_array) / 4), 4, index + 1)
    plt.plot(gamma_table(gamma))
```

![スクリーンショット 2023-01-08 21.53.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e094180a-c48d-70c7-e6ad-16eeaa54cf3f.png)

### 複数のグラフを重ねて表示

ガンマの値を変えた複数のグラフを重ねて表示します

`pyplot.plot` の `label` 引数に値を指定することで自動的に色を変えて表示できます

`pyplot.legend` で凡例を表示します

```python
for index, gamma in enumerate(gamma_array):
    plt.plot(gamma_table(gamma), label=str(gamma))

plt.legend() # 凡例を表示
plt.show()
```

![スクリーンショット 2023-01-08 19.22.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/56956e46-ae0b-c2de-5afa-e5d825bba8e7.png)

## Elixir Livebook で画像・グラフを並べる

### 実行環境

- Elixir 1.14.2 OTP 24
- Livebook 0.8.0

### セットアップ

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

インストールすべきモジュールが必ず明示されるため、再現性は高いです

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

- evision: OpenCV 画像処理
- nx: 行列演算
- req: HTTP リクエスト
- kino: 入出力補助
- kino_vega_lite: グラフ出力

演算子で行列演算を表現できるように Nx.Defn をインポートします

```elixir
import Nx.Defn
```

### 画像の取得

画像を Web から取得してきます

Elixir　の特徴であるパイプ演算子 `|>` によって、一連の処理を繋げて表現できます

`|>` すると、前の関数の結果が次の関数の第1引数に渡されます

以下のコードでは URL からデータを取得し、取得したデータから body を取り出し、body を Evision のマトリックス（画像として扱える形式）にデコードしています

```elixir
org_img =
  "https://github.com/opencv/opencv/blob/master/samples/data/butterfly.jpg?raw=true"
  |> Req.get!()  # URL を指定して画像のバイナリデータをダウンロード
  |> then(& &1.body)
  |> Evision.imdecode(Evision.Constant.cv_IMREAD_COLOR())  # バイナリを Evision で画像としてデコード
```

Livebook では、画像として扱える Evision のマトリックスがセル内の最後の結果になっている場合、自動的に画像を表示してくれます

![スクリーンショット 2023-01-08 19.29.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/66314091-a336-817f-7776-2acb7e863dbe.png)

ちなみに画像の上にあるタブで表示を切り替えることが可能です

![evision.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b4ddf173-39e4-f3c9-4cef-a1264dfe4d1c.gif)

- Image: 画像
- Raw: Evision マトリックス
- Numerical: Nx テンソル

このようなタブ表示が簡単にできるのも Livebook の特長です

https://qiita.com/RyoWakabayashi/items/fd69b0fc73d65b1e3df6

### ガンマ補正

`defn` を使いたいため、モジュールを定義します

`Nx.iota` で指定した形の等差数列を生成し、 0 から 255 の範囲に切り取って、整数に変換しています

```elixir
defmodule Image do
  # ガンマ補正用変換テーブルの生成
  defn gamma_table(gamma) do
    (Nx.iota({256, 1}) / 255) ** (1 / gamma) * 255
    |> Nx.clip(0, 255)
    |> Nx.as_type(:u8)  # 整数に変換
  end

  # ガンマ補正
  def gamma_correction(img, gamma) do
    table = gamma_table(gamma)
    Evision.lut(img, table)
  end
end
```

ガンマ補正後の画像もそのまま表示できます

```elixir
# ガンマ補正を実行、補正結果を表示
corrected_img = Image.gamma_correction(org_img, 2.0)
```

![スクリーンショット 2023-01-08 19.35.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/dbbd9f48-5e28-4eb9-8eda-118fc15ef497.png)

### 元画像と補正後画像を並べて表示

ここに関しては Livebook が圧倒的に分かりやすいです

表示したいものを配列として `Kino.Layout.grid` に渡すだけで並べて表示してくれます

更に `columns` で列数を指定すればそれだけで横に並べることもできます

配列の長さを気にしなくていいのが素晴らしいです

```elixir
Kino.Layout.grid([org_img, corrected_img], columns: 2)
```

![スクリーンショット 2023-01-08 19.40.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/1ff95878-b922-bb77-63f2-d1927947b684.png)

### 値による画像の変化を表示

Elixir では `Enum.map` で配列の各要素をループすることができます

```elixir
# ガンマの値を配列で指定
gamma_list = [0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0]
```

`gamma_list` の各値 `gamma` でガンマ補正した結果をそのまま配列として `Kino.Layout.grid` に渡せば４列に並べて表示してくれます

```elixir
gamma_list
|> Enum.map(fn gamma -> Image.gamma_correction(org_img, gamma) end)
|> Kino.Layout.grid(columns: 4)  # 4列で表示
```

![スクリーンショット 2023-01-08 19.42.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/a5194464-7720-c9b9-c259-27a2a221fb47.png)

画像をどう表示するかについて、ほとんど考える必要がありません

しかも `Layout.grid(columns: 4)` という表記は、一目で「ここで４列に並べて表示しているんだな」と分かる最高の可読性だと思います

### グラフ表示

Python の matplotlib と比べると、 Elixir の Kino.VegaLite はキチンと指定しないと良しなに解釈はしてくれません

ガンマ補正用変換テーブルをグラフ化する場合、ちゃんと X 軸用に 0 から 255 の配列を作ってあげます

また、 `type` の指定が重要です

- quantitative: 数値データ
- temporal: 日付、時刻データ
- ordinal: 序数=ランクなど、数字の大小に意味がある値
- nominal: カテゴリ=IDなど、数字の大小に意味がない値

```elixir
plot_data =
  %{
    input: Enum.to_list(0..255),
    output: Image.gamma_table(2.0) |> Nx.to_flat_list()
  }

VegaLite.new()
|> VegaLite.data_from_values(plot_data)
|> VegaLite.mark(:line)
|> VegaLite.encode_field(:x, "input", type: :quantitative)
|> VegaLite.encode_field(:y, "output", type: :quantitative)
```

![gamma.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5262ee0c-5017-0e18-ff6c-2e840c64a7d3.png)

表示したグラフは SVG や PNG としてそのまま保存できます

![save_graph.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/aff644f0-ba77-2714-dcb9-fff51b372a30.gif)

### 値によるグラフの変化を表示

グラフも画像と同様に `Kino.Layout.grid` で並べることができます

ただし、グラフの横幅を指定しないと、全体が見えなくなることがあります

```elixir
gamma_list
|> Enum.map(fn gamma ->
  %{
    input: Enum.to_list(0..255),
    output: Image.gamma_table(gamma) |> Nx.to_flat_list()
  }

  VegaLite.new(width: 150) # グラフの全体が見えるように横幅を指定
  |> VegaLite.data_from_values(plot_data)
  |> VegaLite.mark(:line)
  |> VegaLite.encode_field(:x, "input", type: :quantitative)
  |> VegaLite.encode_field(:y, "output", type: :quantitative)
end)
|> Kino.Layout.grid(columns: 4)
```

![スクリーンショット 2023-01-08 20.21.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e9062360-1f0d-69da-8644-e7c902944a62.png)

### 複数のグラフを重ねて表示

`Kino.VegaLite` では、項目を `color` として指定することで複数表示することが可能です

この場合、当然 `color` 用の項目も準備しないといけないので、少しコードが複雑になってしまいます

```elixir
plot_data =
  gamma_list
  |> Enum.reduce(%{gamma: [], input: [], output: []}, fn gamma, acc ->
    %{
      gamma: acc.gamma ++ List.duplicate(gamma, 256),
      input: acc.input ++ Enum.to_list(0..255),
      output: acc.output ++ (Image.gamma_table(gamma) |> Nx.to_flat_list())
    }
  end)

VegaLite.new()
|> VegaLite.data_from_values(plot_data)
|> VegaLite.mark(:line)
|> VegaLite.encode_field(:x, "input", type: :quantitative)
|> VegaLite.encode_field(:y, "output", type: :quantitative)
|> VegaLite.encode_field(:color, "gamma", type: :nominal)
```

![multi_gamma.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/3bbe59fb-55bf-f20b-b9be-37c9734a105e.png)

## まとめ

グラフ表示に関しては、 Python の matplotlib がかなり上手く解釈してコード量を少なくしています

既に整形されたデータであれば Elixir でもそこまで手間は必要ありませんが、単に配列をグラフ表示したい場合には Python の方が今のところ簡単です

逆に画像表示や複数結果を並べる場合、 Elixir Livebook の方がかなり簡単で直感的でした