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

この記事は、[Elixir Advent Calendar 2021](https://qiita.com/advent-calendar/2021/elixir)の14日目です
昨日は @Yoosuke さんの[「LiveViewを使って簡単にステートフルなタイピングゲームアプリを作ろう！前編」](https://qiita.com/Yoosuke/items/8ef22f42fb4aaacf4a95)でした

---

[ElixirImp／fukuoka.ex／kokura.ex](https://fukuokaex.connpass.com)と[LiveView JP](https://liveviewjp.connpass.com/)の [piacere](https://twitter.com/piacere_ex) です、ご覧いただいてありがとございます :bow:

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

Livebookを使うと、最も気軽にElixirを始められるので、Elixirが初めての方にも今回コラムはオススメです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/519766ee-6333-85e0-f507-3b1abf31ef97.png)

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

# 本コラムの検証環境

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

## 検証環境①：Windows＋WSL2 Ubuntu 18.04

- Windows 11＋WSL2 Ubuntu 18.04
- Elixir 1.13.0 on WSL2 Ubuntu　※<b>[最新版のインストール手順はコチラ](https://qiita.com/piacerex/items/6b64dfe70f48c89d8385)</b>
- git 2.17.1

## 検証環境②：Ubuntu 18.04

- Ubuntu 18.04
- Elixir 1.13.0　　 ※<b>[最新版のインストール手順はコチラ](https://qiita.com/piacerex/items/6b64dfe70f48c89d8385)</b>
- sudo apt installで下記インストール
  - git
  - erlang-dev
  - erlang-os-mon
  - erlang-xmerl
  - erlang-parsetools

# 事前準備：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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/cc61d797-6cdd-23b0-c41c-0a1014e0f385.png)

「New notebook」を開くと、下記画面が表示され、ここでElixirコードを実行することが出来ます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/21669331-90b5-93e5-897e-65188959b82d.png)

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

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

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

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

実行した画面は、以下のようになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a337a954-cd73-fd37-0ec4-72febdc41a6f.png)

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

Livebook上で、コードブロックの実行結果の下にカーソルを持っていくと、下記にように、ブロック追加のためのボタンが表示されるので、「+Elixir」ボタンをクリックしてください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/da726005-b4b4-e3b9-8bfb-f51576ad72ea.png)

すると、下記のように、コードブロックが追加されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/36a7f70e-f909-28f0-cdbb-b2faa807fb37.png)

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

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

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

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

下図のように実行できれば成功です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/151d2ebd-06cd-bc4e-7f65-7b624e425db8.png)

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

```elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/196e68ac-b888-784a-0278-bd4e4fbd961d.png)

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

https://github.com/elixir-nx/nx/blob/main/nx/lib/nx.ex

もしくは、Nxをgit cloneし、nxフォルダ配下でmix docsコマンドを叩き、出力されるdocsをご覧ください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/082c0c15-e62b-9670-0f79-eb8145e16289.png)

## 画像をLivebookで表示する

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

ただしLivebookでは、ノートブック内で何度もライブラリ追加を行うことは出来ず、下図のようにエラーとなります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/9e03f177-363d-330d-de0b-ebdc4a93da13.png)

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

```elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/8119b60f-ac71-b93b-3800-2e045ad5184a.png)

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

```elixir: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でダウンロードした画像を表示してみます

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

いつものlennaさんが、上手く表示できました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/b5a81ca8-85ba-fa51-81ff-bf8fed5feac4.png)

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

```elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/945129fa-c204-842b-88d6-54e25453be9c.png)

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

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

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

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

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

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

```elixir: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さんの画像を行列に変換する前に、画像サイズと画像ピクセルデータを確認します

```elixir:Livebook上
lenna
|> Pixels.read_file
```

512 x 512の画像であることが確認でき、画像ピクセルデータは「R／G／B／A」の4バイトで1ピクセルを構成しています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/98aa0c70-7a2a-0471-a47c-6736070be3ad.png)

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

```elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a6e74955-8918-bf57-cfc4-286751ec852d.png)

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

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

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

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

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

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

現時点では画像加工を行っていないため、元の画像ピクセルデータと同一のデータが返ってきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/d0ef7a68-1048-875f-ea2d-5dc6c4cb376b.png)

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

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

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

```elixir:Livebook上①
rgba_tuple_list = changed
  |> Enum.chunk_every(4)
  |> Enum.map(fn [r, g, b, a] -> {r, g, b, a} end)
```

```elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/681ce7f6-e750-b6b5-a835-e46cdecdc07f.png)

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

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

無変換の画像が、無事に表示できました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/2495a461-20e8-8667-cff0-366a2c7e363b.png)

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

# 画像加工を行う

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

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

## ネガポジ反転

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

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

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

```elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a0e60d6a-7e64-750a-e51a-b37a1e4ad8f5.png)

## グレースケール

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

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

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

```elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/0228617a-87c9-5540-b0bf-8957993586d3.png)

# 終わり

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

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

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

明日は @kikuyuta さんで[「世俗派関数型言語 Elixir を関数型言語風に使ってみたらやっぱり関数型言語みたいだった」](https://qiita.com/kikuyuta/items/afa4c264720eb29d9760)です

# 主催／運営している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コミュニティ)](https://fukuokaex.connpass.com)

https://fukuokaex.connpass.com

[**4. LiveView JP** : A place to mob-program in LiveView, LiveBook+Nx+Axon, and elixir-desktop](https://liveviewjp.connpass.com)

https://liveviewjp.connpass.com

[**5. Neos.ex** : A place to connecting Elixir and NeosVR to create a new world](https://elixirjp.slack.com/archives/C02J9C12VMF)

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

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

本コラムも、[第3弾「Elixir／Livebook＋NxでPythonっぽくAI・ML」](https://qiita.com/piacerex/items/e5590fa287d3c89eeebf#%E7%AC%AC3%E5%BC%BEelixirlivebooknx%E3%81%A7python%E3%81%A3%E3%81%BD%E3%81%8Faiml)に追加しています

https://qiita.com/piacerex/items/e5590fa287d3c89eeebf#%E7%AC%AC3%E5%BC%BEelixirlivebooknx%E3%81%A7python%E3%81%A3%E3%81%BD%E3%81%8Faiml

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/67568d54-6584-ad33-77c7-b3b3afe70287.png) や ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/f5b819ef-d655-ef72-294c-b1a9d2c5b563.png) にて、どうぞ応援よろしくお願いします:bow