はじめに
Elixir Livebook は、ブラウザ上でコードを実行して結果を表示するツールです
画像もグラフも表も地図も簡単に表示できます
ちなみに別の言語も実行できます
AI・機械学習にも使えます
そして Elixir は API も Web サイトもデスクトップアプリ(Windows、macOS、Linux)もモバイルアプリ(iOS、Android)も実装できるため、 Livebook でやったことは何にでも組み込めます
要するに Elixir の Livebook は Python の Jupyter です
今回は画像・グラフの並べ方を例として、両者を比較してみます
例としてガンマ補正の画像表示、グラフ表示を実装してみます
参考サイト
Python のガンマ補正実装はこちらのサイトを参考にしました
他のサイトでは for 文を使って変換テーブルを生成していることが多いのですが、 for ループは非常に遅いので、せっかく numpy を使うのであれば、ちゃんと numpy.arange
を使うこちらの実装が良いと思います
Python Jupyter で画像・グラフを並べる
実行環境
- Python 3.10.9
- JupyterLab 3.5.2
セットアップ
必要なモジュールをインポートします
Jupyter の場合、各環境にインストール済のモジュールをそのまま使います
もし Jupyter 上からインストールする場合は !
に続けてシェルコマンドが実行できます
# !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
を使用します
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)
ガンマ補正
後でガンマ補正用テーブルのグラフを表示するため、テーブル生成を別関数にしています
numpy.arange
で 0 から 255 の等差数列を生成し、ガンマ補正の計算を実行します
cv2.LUT
で変換テーブルに従って 0 から 255 の値を変換します
# ガンマ補正用変換テーブルの生成
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)
# ガンマ補正を実行
corrected_img = gamma_correction(org_img, 2.0)
# ガンマ補正結果を表示
plt.imshow(corrected_img)
元画像と補正後画像を並べて表示
複数の画像やグラフを並べて表示する場合、 pyplot.subplot
で位置を指定します
- 第1引数: 行数(縦に何分割するか)
- 第2引数: 列数(横に何分割するか)
- 第3引数: 左上からZ字に進むとき何番目か
# 元画像
plt.subplot(1, 2, 1) # 1行2列の表示域の1番目
plt.imshow(org_img)
# 補正後画像
plt.subplot(1, 2, 2) # 1行2列の表示域の2番目
plt.imshow(corrected_img)
ちなみに、省略した書き方も可能です
第1引数 * 100 + 第2引数 * 10 + 第3引数 を第1引数に渡すことで同じ結果になります
# 元画像
plt.subplot(121)
plt.imshow(org_img)
# 補正後画像
plt.subplot(122)
plt.imshow(corrected_img)
簡略に書ける一方、可読性が高いとは言えないでしょう
値による画像の変化を表示
Python では for x in array:
で配列の要素をループできます
# ガンマの値を配列で指定
gamma_array = [0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0]
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))
グラフ表示
グラフを表示する場合、 pyplot.plot
に Numpy 配列を渡すだけでいい感じに解釈して線グラフとして表示してくれます
table = gamma_table(2.0)
plt.plot(table)
値によるグラフの変化を表示
グラフも画像と同様に subplot
を使えば並べてくれます
ただし、 subplots_adjust
で間隔を調整しないとグラフ同士が重なってしまうことがあります
# グラフ間の横間隔を調整する
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))
複数のグラフを重ねて表示
ガンマの値を変えた複数のグラフを重ねて表示します
pyplot.plot
の label
引数に値を指定することで自動的に色を変えて表示できます
pyplot.legend
で凡例を表示します
for index, gamma in enumerate(gamma_array):
plt.plot(gamma_table(gamma), label=str(gamma))
plt.legend() # 凡例を表示
plt.show()
Elixir Livebook で画像・グラフを並べる
実行環境
- Elixir 1.14.2 OTP 24
- Livebook 0.8.0
セットアップ
必要なモジュールをインストールします
インストールすべきモジュールが必ず明示されるため、再現性は高いです
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 をインポートします
import Nx.Defn
画像の取得
画像を Web から取得してきます
Elixir の特徴であるパイプ演算子 |>
によって、一連の処理を繋げて表現できます
|>
すると、前の関数の結果が次の関数の第1引数に渡されます
以下のコードでは URL からデータを取得し、取得したデータから body を取り出し、body を Evision のマトリックス(画像として扱える形式)にデコードしています
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 のマトリックスがセル内の最後の結果になっている場合、自動的に画像を表示してくれます
ちなみに画像の上にあるタブで表示を切り替えることが可能です
- Image: 画像
- Raw: Evision マトリックス
- Numerical: Nx テンソル
このようなタブ表示が簡単にできるのも Livebook の特長です
ガンマ補正
defn
を使いたいため、モジュールを定義します
Nx.iota
で指定した形の等差数列を生成し、 0 から 255 の範囲に切り取って、整数に変換しています
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
ガンマ補正後の画像もそのまま表示できます
# ガンマ補正を実行、補正結果を表示
corrected_img = Image.gamma_correction(org_img, 2.0)
元画像と補正後画像を並べて表示
ここに関しては Livebook が圧倒的に分かりやすいです
表示したいものを配列として Kino.Layout.grid
に渡すだけで並べて表示してくれます
更に columns
で列数を指定すればそれだけで横に並べることもできます
配列の長さを気にしなくていいのが素晴らしいです
Kino.Layout.grid([org_img, corrected_img], columns: 2)
値による画像の変化を表示
Elixir では Enum.map
で配列の各要素をループすることができます
# ガンマの値を配列で指定
gamma_list = [0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0]
gamma_list
の各値 gamma
でガンマ補正した結果をそのまま配列として Kino.Layout.grid
に渡せば4列に並べて表示してくれます
gamma_list
|> Enum.map(fn gamma -> Image.gamma_correction(org_img, gamma) end)
|> Kino.Layout.grid(columns: 4) # 4列で表示
画像をどう表示するかについて、ほとんど考える必要がありません
しかも Layout.grid(columns: 4)
という表記は、一目で「ここで4列に並べて表示しているんだな」と分かる最高の可読性だと思います
グラフ表示
Python の matplotlib と比べると、 Elixir の Kino.VegaLite はキチンと指定しないと良しなに解釈はしてくれません
ガンマ補正用変換テーブルをグラフ化する場合、ちゃんと X 軸用に 0 から 255 の配列を作ってあげます
また、 type
の指定が重要です
- quantitative: 数値データ
- temporal: 日付、時刻データ
- ordinal: 序数=ランクなど、数字の大小に意味がある値
- nominal: カテゴリ=IDなど、数字の大小に意味がない値
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)
表示したグラフは SVG や PNG としてそのまま保存できます
値によるグラフの変化を表示
グラフも画像と同様に Kino.Layout.grid
で並べることができます
ただし、グラフの横幅を指定しないと、全体が見えなくなることがあります
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)
複数のグラフを重ねて表示
Kino.VegaLite
では、項目を color
として指定することで複数表示することが可能です
この場合、当然 color
用の項目も準備しないといけないので、少しコードが複雑になってしまいます
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)
まとめ
グラフ表示に関しては、 Python の matplotlib がかなり上手く解釈してコード量を少なくしています
既に整形されたデータであれば Elixir でもそこまで手間は必要ありませんが、単に配列をグラフ表示したい場合には Python の方が今のところ簡単です
逆に画像表示や複数結果を並べる場合、 Elixir Livebook の方がかなり簡単で直感的でした