Elixir Livebook ノートブックで画像・グラフを横に並べて表示する

## はじめに

Elixir Livebook で画像処理やグラフ表示をしているとき、結果を横に並べて比較したい時がありますよね

というわけで画像やグラフを横に並べる方法です

タブ切り替えで表示する場合はこちら

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

## 実行環境

- Elixir: 1.14.2 OTP 24
- Livebook: 0.8.0

## セットアップ

画像とグラフの表示に最低限必要なモジュールをインストールします

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

## 画像を並べる

指定した色の画像を生成する関数を用意します

```elixir
generate_image = fn color ->
  {height, width} = {160, 160}

  color
  |> Nx.tensor(type: :u8)
  |> Nx.tile([height, width, 1])
  |> Kino.Image.new()
end
```

例えば以下のように書いた場合、一番最後の画像だけが表示されてしまいます

```elixir
generate_image.([255, 0, 0])
generate_image.([0, 255, 0])
generate_image.([0, 0, 255])
generate_image.([255, 255, 0])
generate_image.([255, 0, 255])
generate_image.([0, 255, 255])
```

![スクリーンショット 2023-01-08 23.16.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/aef42aba-4362-37f0-97c5-dc35ca3445c8.png)

以下のように `Kino.render` を入れると、途中でも結果を表示することができます

ただし、縦に並ぶので多くなると見にくいです

```elixir
generate_image.([255, 0, 0])
generate_image.([0, 255, 0])
generate_image.([0, 0, 255])
|> Kino.render()
generate_image.([255, 255, 0])
generate_image.([255, 0, 255])
|> Kino.render()
generate_image.([0, 255, 255])
```

![スクリーンショット 2023-01-08 23.17.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/d54cc4cf-05a6-facf-4ba9-9070749f72bc.png)

表示したいものを配列に入れて `Kino.Layout.grid` に渡します

そのとき、 `columns` に列数を指定しておくと、その分横に並んで見やすくなります

```elixir
[
  generate_image.([255, 0, 0]),
  generate_image.([0, 255, 0]),
  generate_image.([0, 0, 255]),
  generate_image.([255, 255, 0]),
  generate_image.([255, 0, 255]),
  generate_image.([0, 255, 255]),
]
|> Kino.Layout.grid(columns: 3)
```

![スクリーンショット 2023-01-08 23.18.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/634fd9b4-b6a8-973e-fa74-83c0413201d7.png)

当然、 `Enum.map` でパイプしてもいいので、パラメータ変化に伴う結果の変化を並べて表示したいときなどに有効です

```elixir
[
  [0, 0, 0], [255, 0, 0], [0, 255, 0], [0, 0, 255],
  [255, 255, 0], [255, 0, 255], [0, 255, 255], [255, 255, 255]
]
|> Enum.map(&generate_image.(&1))
|> Kino.Layout.grid(columns: 4)
```

![スクリーンショット 2023-01-08 23.19.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/fa6294c7-3423-3003-4a4e-c50226419ecf.png)

`boxed` （デフォルトは false） に true を指定すると、出力全体の外側に枠が付きます

`gap` で各出力の間隔を指定できます

```elixir
[
  [255, 0, 0], [0, 255, 0], [0, 0, 255],
  [255, 255, 0], [255, 0, 255], [0, 255, 255]
]
|> Enum.map(&generate_image.(&1))
|> Kino.Layout.grid(columns: 3, boxed: true, gap: 60)
```

![スクリーンショット 2023-01-08 23.20.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/0ff6d7af-e112-b14a-1291-1b00961da615.png)

ネストする（入れ子にする）こともできるため、複雑な形にすることも可能です

```elixir
[
  generate_image.([255, 0, 0]),
  generate_image.([0, 0, 255]),
  [
    generate_image.([255, 0, 0]),
    generate_image.([0, 0, 255]),
    generate_image.([0, 0, 255]),
    generate_image.([255, 0, 0]),
  ]
  |> Kino.Layout.grid(columns: 2, gap: 10),
  generate_image.([0, 0, 255]),
  generate_image.([255, 0, 0]),
]
|> Kino.Layout.grid(columns: 5, gap: 10)
```

![スクリーンショット 2023-01-08 23.20.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/bd31dedd-0d92-87b3-b4cb-66674909d865.png)

## グラフの場合

$ y = x^p $ のグラフをプロットする関数を用意します

```elixir
plot = fn p ->
  x = Nx.iota({10})
  y = Nx.power(x, p)

  VegaLite.new(width: 180)
  |> VegaLite.data_from_values(%{x: Nx.to_flat_list(x), y: Nx.to_flat_list(y)})
  |> VegaLite.mark(:line)
  |> VegaLite.encode_field(:x, "x", type: :quantitative)
  |> VegaLite.encode_field(:y, "y", type: :quantitative)
end
```

画像と同じように、 `Kino.Layout.grid` で並べることができます

```elixir
0..8
|> Enum.map(&plot.(&1))
|> Kino.Layout.grid(columns: 3)
```

![スクリーンショット 2023-01-08 23.21.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9c7323f7-9730-e8a1-0935-2d3524a04570.png)

## まとめ

`Kino.Layout.grid` によって画像やグラフを自在に並べることができました

もちろん他の出力も全て同様に扱うことが可能なので、様々な応用で表現力を向上させましょう