LoginSignup
9

More than 1 year has passed since last update.

「JupyterNotebook + NumPyでサクッと画像加工するノリ」をElixir Livebookでやってみた(lennaさんのバージョンアップもあるよ)

Last updated at Posted at 2021-12-16

この記事は、Elixir Advent Calendar 2021の14日目です
昨日は @Yoosuke さんの「LiveViewを使って簡単にステートフルなタイピングゲームアプリを作ろう!前編」でした


ElixirImp/fukuoka.ex/kokura.exLiveView JPpiacere です、ご覧いただいてありがとございます :bow:

今回は、PythonのJupyterNotebook + NumPyで画像加工するノリで、「JupyterNotebook」のElixir版である「Livebook」と、「NumPy」のElixir版である「Nx」による画像加工(ネガポジ反転、グレースケール)をやってみました

Livebookを使うと、最も気軽にElixirを始められるので、Elixirが初めての方にも今回コラムはオススメです
image.png

内容が、面白かったり、役に立ったら、「LGTM」よろしくお願いします :wink:

本コラムの検証環境

本コラムは、以下環境で検証しています(Windows WSL2で実施していますが、Windowsネイティブや他Linuxディストリビューション、macでも動作する想定です)

検証環境①:Windows+WSL2 Ubuntu 18.04

検証環境②:Ubuntu 18.04

事前準備:Livebook + Nx + 画像表示

LivebookをローカルPCで起動

下記コマンドでLivebook最新版をダウンロードし、起動します

git clone https://github.com/elixir-nx/livebook.git
cd livebook
mix deps.get --only prod
MIX_ENV=prod mix phx.server

下記のように待ち受けログが出れば、起動成功です

[Livebook] Application running at http://localhost:8080/?token=a55u2laaty53acnvyokw27ucjx7yrjfu

上記ログに書かれたURLをブラウザでアクセスすると、下記のような画面が表示されます
image.png

「New notebook」を開くと、下記画面が表示され、ここでElixirコードを実行することが出来ます
image.png

NxをLivebookで使えるようにする

Pythonで言うところのNumPyと同様の行列計算ライブラリ「Nx」をLivebookで使えるようにします

通常のElixir/Phoenixプロジェクトであれば、mix.exsのdepsにライブラリを追加することで対応しますが、Livebookの場合は、ライブラリ追加もノートブックにて行えるので、下記コードをLivebook上のコードブロック(Elixirコードを入力する枠)に入力し、「Evaluate」ボタンをクリックするか、「Ctrl」+「Enter」キーで実行してください

Livebook上
Mix.install(
  [
    {:nx, "~> 0.1.0-dev", github: "elixir-nx/nx", branch: "main", sparse: "nx"}
  ]
)

実行した画面は、以下のようになります
image.png

Nxが使えるか試してみましょう

Livebook上で、コードブロックの実行結果の下にカーソルを持っていくと、下記にように、ブロック追加のためのボタンが表示されるので、「+Elixir」ボタンをクリックしてください
image.png

すると、下記のように、コードブロックが追加されます
image.png

ここに、下記コードを書いて、実行してみましょう

Livebook上
Nx.tensor([[1, 2], [3, 4]])
|> Nx.divide(3)

このコードは、下記のような行列をNx.tensor()で作り、各値を3で除算するというNxのコードです

\begin{bmatrix}
1 & 2 \\
3 & 4
\end{bmatrix}

下図のように実行できれば成功です
image.png

行列のヒートマップ表示も、このように簡単です

Livebook上
Nx.tensor([[9, 8, 7, 6, 5], [8, 7, 6, 5, 4], [7, 6, 5, 4, 3], [6, 5, 4, 3, 2], [5, 4, 3, 2, 1]])
|> Nx.to_heatmap

image.png

他にも、NumPyの配列であるndarrayのバイナリ保存されたものをロードするfrom_numpy()や、行列をフラットなリストに変換するto_flat_list()等、便利な関数ありますが、NxはHexDocsがアップされていないため、関数をご自身で調べたい方は、下記ソースコードのdefを直接見てください

もしくは、Nxをgit cloneし、nxフォルダ配下でmix docsコマンドを叩き、出力されるdocsをご覧ください
image.png

画像をLivebookで表示する

画像表示のためにLivebook用レンダーライブラリ「Kino」とダウンロードライブラリ「Download」を追加します

ただしLivebookでは、ノートブック内で何度もライブラリ追加を行うことは出来ず、下図のようにエラーとなります
image.png

そのため、先頭のライブラリ追加を下記に書き換え、再実行します(その際、一度エラーを出した後は、「Restart runtime」ボタンをクリックしないとエラーが解消されません)

Livebook上
Mix.install(
  [
    {:nx, "~> 0.1.0-dev", github: "elixir-nx/nx", branch: "main", sparse: "nx"}, 
    {:kino, "~> 0.3.1"},
    {:download, "~> 0.0.4"}
  ]
)

これで、下図のようにライブラリ追加に成功します
image.png

まず、Downloadで画像をダウンロードしましょう

Livebook上⑫
File.rm("Lenna_%28test_image%29.png")  # 再実行時、Download.from()でeexistエラーになるのを防止

lenna = Download.from("https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png")
  |> elem(1)

次に、Kinoでダウンロードした画像を表示してみます

Livebook上②
lenna
|> File.read!
|> Kino.Image.new(:jpeg)

いつものlennaさんが、上手く表示できました
image.png

ちなみに下記コードだと、lennaさんをバージョンアップできます(が、jpgファイルなので、この後、扱いません) :wink:

Livebook上
File.rm("D2_x0z7UkAAI4gJ.jpg")

Download.from("https://pbs.twimg.com/media/D2_x0z7UkAAI4gJ.jpg")
|> elem(1)
|> File.read!
|> Kino.Image.new(:jpeg)

image.png

ここまでで、Livebookの基礎的な道具は揃いました

行列による画像加工の土台を作る

画像加工を行う前に、元画像を無変換で表示できるようにすることで、画像加工検証に使える土台を作ります

画像をRGBA形式の行列に変換

バージョンアップ前のlennaさんはpngファイルなので、この後、画像処理しやすいよう、RGBA形式に変換します

まず、先頭のライブラリにpng操作ライブラリ「Pixels」「Pngex」を追加します

Livebook上
Mix.install(
  [
    {:nx, "~> 0.1.0-dev", github: "elixir-nx/nx", branch: "main", sparse: "nx"}, 
    {:kino, "~> 0.3.1"},
    {:download, "~> 0.0.4"},
    {:pixels, "~> 0.2.1"},
    {:pngex, "~> 0.1.1"}
  ]
)

それから、lennaさんの画像を行列に変換する前に、画像サイズと画像ピクセルデータを確認します

Livebook上
lenna
|> Pixels.read_file

512 x 512の画像であることが確認でき、画像ピクセルデータは「R/G/B/A」の4バイトで1ピクセルを構成しています
image.png

つまり、4データ x 262,144ピクセル(≒512 x 512)の行列に割り当てれば、1ピクセル毎の処理がしやすくなるので、下記コードで、画像ピクセルデータをNx.from_binary()Nx.reshape()でこの形式の行列に変換します

Livebook上①
rgba = lenna
  |> Pixels.read_file
  |> elem(1)
  |> Map.get(:data)
  |> Nx.from_binary({:u, 8})
  |> Nx.reshape({262144, 4})

RGBAで1ピクセルを1行とする行列ができました
image.png

画像加工のための枠を設ける

画像加工処理を後で入れるための枠を設け、加工後の行列を画像ピクセルデータに戻す処理を作ります

多次元行列であっても、Nx.to_flat_list()を使えば、1次元リストに変換できます

なお今回未使用ですが、後続処理でバイナリデータに変換したい場合は、Enum.into()を使います

Livebook上
# ここから画像加工処理
new_rgba = rgba
# ここまで画像加工処理

changed = new_rgba
  |> Nx.to_flat_list
#  |> Enum.into(<<>>, & <<&1::8>>)  # リストでは無くバイナリデータにしたいときの書き方

現時点では画像加工を行っていないため、元の画像ピクセルデータと同一のデータが返ってきます
image.png

png画像として保存し、画像表示する

Pngex.new()Pngex.generate()を使うと、png画像が生成できるので、File.write()でファイルとして保存できます

Pngex.generate()に渡すデータは、RGBAのタプルのリストなので、Enum.chunk_every()で4つ1組のリストへと変換し、Enum.map()でリストからタプルに変換します

Livebook上①
rgba_tuple_list = changed
  |> Enum.chunk_every(4)
  |> Enum.map(fn [r, g, b, a] -> {r, g, b, a} end)
Livebook上②
image = Pngex.new(type: :rgba, depth: :depth8, width: 512, height: 512)
  |> Pngex.generate(rgba_tuple_list)

File.write("new.png", image)

pngファイル保存まで出来ました
image.png

保存したファイルを表示します

Livebook上
"new.png"
|> File.read!
|> Kino.Image.new(:png)

無変換の画像が、無事に表示できました
image.png

これで、画像加工処理の土台ができたので、画像加工処理をやっていきましょう

画像加工を行う

それでは、上記「画像加工のための枠を設ける」で作った枠を、下記の画像加工処理で入れ替えていきます

  • ネガポジ反転
  • グレースケール

ネガポジ反転

ネガポジ反転は、RGBの各値を、MAXである255から引くと生成できます

そのために、RGBAの「RGB」と「A」を分離し、RGBの各値を255から引いた後、RGBとAをNx.concatenate()で再度、結合します

下記コードで、該当コードブロックを入れ替えてみましょう

Livebook上
# ここから画像加工処理
#new_rgba = rgba
rgb = rgba
  |> Nx.slice_axis(0, 3, -1)
  |> Nx.map(& Nx.subtract(255, &1))
a = rgba
  |> Nx.slice_axis(4, 1, -1)
new_rgba = Nx.concatenate([rgb, a], axis: -1)

# ここまで画像加工処理

changed = new_rgba
  |> Nx.to_flat_list
#  |> Enum.into(<<>>, & <<&1::8>>)  # リストでは無くバイナリデータにしたいときの書き方

以降のコードブロックを実行した結果は、下図の通り、ネガポジ反転した画像を作成できました
image.png

グレースケール

グレースケールは、RGBの各値の平均値をNx.mean()で取り、その値をRGBの各値に設定すると生成できます

こちらもネガポジ反転と同様、「RGB」と「A」を分離した後、RGBを加工し、結合します

下記コードで、該当コードブロックを入れ替えてみましょう

Livebook上
# ここから画像加工処理
#new_rgba = rgba
rgb = rgba
  |> Nx.slice_axis(0, 3, -1)
  |> Nx.mean(axes: [-1])
  |> Nx.round()
  |> Nx.as_type({:u, 8})
  |> Nx.to_flat_list
  |> Enum.map(& [&1, &1, &1])
  |> Nx.tensor
a = rgba
  |> Nx.slice_axis(4, 1, -1)
new_rgba = Nx.concatenate([rgb, a], axis: -1)
# ここまで画像加工処理

changed = new_rgba
  |> Nx.to_flat_list
#  |> Enum.into(<<>>, & <<&1::8>>)  # リストでは無くバイナリデータにしたいときの書き方

以降のコードブロックを実行した結果は、下図の通り、グレースケールした画像を作成できました
image.png

終わり

fukuoka.ex Elixir/Phoenix Advent Calendar 2021をご覧になっている方は、お気付きになったかも知れませんが、今回のネタは、「LiveViewとNxで画像処理」 という @the_haigo さんのコラムのLivebookバージョンでした :yum:

Livebookを使えば、ノートブック形式で、気軽にビジュアルも扱えるElixir開発ができるということが伝われば幸いです

それと、今回は画像処理でしたが、JupyterNotebookやColaboratoryが、AI・ML開発に利用できるのと同様に、Elixir/LivebookでもAI・ML開発ができる…というノウハウを2022年は開放していこうと思っているので、ワクワクしてお待ちください

明日は @kikuyuta さんで「世俗派関数型言語 Elixir を関数型言語風に使ってみたらやっぱり関数型言語みたいだった」です

主催/運営しているElixirコミュニティ紹介

1. ElixirImp : A place to LOVE the buds in Elixir (Elixir実装の芽を愛でる場)
2. fukuoka.ex : Fukuoka local Elixir Community (福岡Elixirコミュニティ)
3. kokura.ex : Kokura local Elixir Community (小倉Elixirコミュニティ)

4. LiveView JP : A place to mob-program in LiveView, LiveBook+Nx+Axon, and elixir-desktop

5. Neos.ex : A place to connecting Elixir and NeosVR to create a new world

:ocean::ocean::ocean: Elixir生誕10周年を祝い、"Elixirの現在" に追いつける :ocean::ocean::ocean:

Elixir界隈に激震をもたらした2021年の大変動を活用するコラム群を日々アップデートしています

本コラムも、第3弾「Elixir/Livebook+NxでPythonっぽくAI・ML」に追加しています

p.s.このコラムが、面白かったり、役に立ったら…

image.pngimage.png にて、どうぞ応援よろしくお願いします:bow

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9