Julia / Jupyter での画像表示を実装する

# ポイント

- Jupyter 上でどうやって画像を表示するのか？
- Jupyter が対応していないフォーマットの画像を表示するには？
- 値型やメタプログラミングは、どういう場面で用いられるのか？

この記事の内容を実装したパッケージ
https://github.com/Lirimy/ImageContainers.jl

![example.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202534/e438a291-b105-9646-b289-58797ec9771b.png)


# はじめに

この記事が想定している状況は、例えば、 Julia から gnuplot や ffmpeg などにデータを送り、得られた画像や動画を Jupyter 上でシームレスに表示して確認したい、といったプロセスである。

この点で、Images.jl, ImageView.jl とは少し状況が異なる。
これらのパッケージは、画像を色の配列として保持している。
その上で、画像自体を編集・加工することができる。

ここでは、目的を Jupyter 上での画像表示のみに絞ることにする。
一方で、色の配列だけではなく、 PNG, SVG などの画像も直接取り扱えるようにしよう。


# Jupyter での画像表示

自分で定義した型のオブジェクトを Jupyter 上で表示することを考えよう。

これは、Julia の特徴である多重ディスパッチを利用して、独自の型に対する `Base.show` 関数を定義すればよい。
https://docs.julialang.org/en/v1/base/io-network/#Base.show-Tuple{IO,Any,Any}

```julia
# PNG のデータをそのまま詰め込む
struct PNGContainer
    content
end

# PNGContainer 型のオブジェクトに対して呼ばれる
# 出力形式を MIME で指定する
function Base.show(io::IO, ::MIME"image/png", c::PNGContainer)
    write(io, c.content)
end

# 実際に PNGContainer 型のオブジェクトをつくると、 Jupyter 上で表示される
c = open("sample.png") do io
	PNGContainer(read(io))
end

display(c)
```

`Base.show` の第一引数 `io` に書き込まれたデータが IJulia.jl, Jupyter ホストを介してブラウザに届き、画像が表示される。



JuliaCon 2020 | Display, show and print -- how Julia's display system works | Fredrik Ekre
https://www.youtube.com/watch?v=S1Fb5oNhhbc

# 様々なフォーマットに対応させる

前節では、PNG の表示ができるようになった。
当然ながら、 SVG なども同じように表示させたいだろう。

しかし、多重ディスパッチでは、「型」に対して呼ばれるメソッドが決まるのであって、「値」によって処理を分岐させることができない。


だからといって、 `SVGContainer` などと次々に型をつくっていくのも不毛だろう。

これを解決するのが[値型](https://docs.julialang.org/en/v1/manual/types/index.html#%22Value-types%22-1)である。

```julia
struct ImageContainer{format}
    content
end

function Base.show(io::IO, ::MIME"image/png", c::ImageContainer{:png})
    write(io, c.content)
end

function Base.show(io::IO, ::MIME"image/svg+xml", c::ImageContainer{:svg})
    write(io, c.content)
end

c = open("sample.svg") do io
	ImageContainer{:svg}(read(io))
end
```

ここでは、画像フォーマットに応じたシンボルによって[^symbolreason]、 `ImageContainer` は別の型だとみなされる。

したがって、多重ディスパッチが効くため、表示させたいすべての画像フォーマットに対して `Base.show` を繰り返し記述すれば、目的は達せられることになる。



[^symbolreason]: 画像フォーマットを表すような「値」が望ましいのだが、値型の仕様で文字列は利用できないため、「値」としてシンボルを用いた



# メタ・プログラミング！

さて前節は、似たようなことを繰り返し記述するのが面倒だ、という話であった。

これこそ[メタプログラミング](https://docs.julialang.org/en/v1/manual/metaprogramming/#Metaprogramming-1)の出番であろう。

```julia
const mimetexts = Dict(
    :png    => "image/png",
    :svg    => "image/svg+xml",
    :jpg    => "image/jpeg",
    :jpeg   => "image/jpeg"
)

# function Base.show(io::IO, ::MIME"image/png", c::ImageContainer{:png})
#     write(io, c.content)
# end

for (fmt, mime) in mimetexts
    @eval function Base.show(io::IO, ::@MIME_str($mime), c::ImageContainer{$(QuoteNode(fmt))})
        write(io, c.content)
    end
end
```

- `@eval` マクロで、表現がグローバルスコープで評価（実行）される
- `$val` で、変数が評価される（補間というらしい）
- `MIME` は[シングルトン](https://docs.julialang.org/en/v1/manual/types/#Composite-Types-1)[^singleton]で、評価に備えて `@MIME_str` が用意されている



[^singleton]: フィールドを持たない複合型で、インスタンスが１つしかない



## 補足：シンボルの評価を逃れるには？

先の例で、 フォーマットを表す変数 `fmt` には、中にシンボルが入っている。
このため、`$fmt` が評価されるとエラーが出てしまう。
評価後にシンボルを残すには、どうすればよいだろうか？

```julia
# 評価後に ImageContainer{:png} を得たい

fmt = :png
@eval ImageContainer{$fmt}
#ERROR: UndefVarError: png not defined
```


### 方法０：QuoteNode

コメントで @antimon2 さんに教えていただきました。

```julia
@eval ImageContainer{$(QuoteNode(fmt))}
#ImageContainer{:png}
```

https://docs.julialang.org/en/v1/manual/metaprogramming/#QuoteNode-1

### 方法１：配列に隠す

配列の要素に隠し、配列を評価したあとで、中から要素を取り出せばよい。

```julia
@eval ImageContainer{$[fmt]...}
#ImageContainer{:png}
```

参考：[Metaprogramming in JuliaLang](https://www.slideshare.net/yuichimotoyama961/slide-47398472) 10 / 38


### 方法２：実体をつくる

実はこちらの方を先に思いつき、後から方法１が見つかった。
結局使わなかったが、もったいないので共有して供養したい。

```julia
dummy = ImageContainer{fmt}(UInt8[])
@eval typeof($dummy)
#ImageContainer{:png}
```

# HTML に画像を埋め込む

ここまでで、 PNG / SVG / JPEG の表示は実装できた。
これができたのは、実は [IJulia.jl](https://github.com/JuliaLang/IJulia.jl) がそれらの MIME タイプを[素直に受け取ってくれる](https://github.com/JuliaLang/IJulia.jl/blob/master/src/display.jl#L19)ためである。



それでは、 IJulia.jl が対応していない画像フォーマットは表示できないのだろうか。

別にそんなことはなく、 `MIME("text/html")` にして、HTML タグを介して出力すれば事足りる。



## 画像データの渡し方

次に検討すべきなのは、画像データの送り方である。
方法は２つある。

1. 一時ファイルに書き出し、リンクする
2. Base64 エンコードして直接出力する


1 はよく使われる方法で、実際に [Plots.jl/animation.jl](https://github.com/JuliaPlots/Plots.jl/blob/master/src/animation.jl#L97) ではそのように実装されている。

いろいろ考えた結果、今回は 2 を採用する。


2 を採用する理由としては、 Jupyter Notebook のファイルに出力画像が埋め込まれることだろうか。
画像の管理が不要になり、 Gist などで Notebook の共有が気軽にできるようになる。

逆に Notebook のファイルサイズが増えたり、バージョン管理が重くなったりするデメリットもある。
（セミコロンで出力を抑制すれば防げるが）


## Base64 エンコード

Base64 とは、バイナリデータをアルファベットなどに対応させ、テキストとして取り扱えるようにするエンコード方式である。



今回は、画像データを Base64 エンコードして、 HTML タグ内に

```html
<img src="data:image/gif;base64,R0lGODdhgALgAeYAAP...isQADs=" />
```

などとして埋め込む。


`Base64.Base64EncodePipe` を通すとエンコードしてくれるので、そのまま実装しよう。

```julia
using Base64

function Base.show(io::IO, ::MIME"text/html", c::ImageContainer{:gif})
    
    write(io, "<img src=\"data:image/gif;base64,")
    
    ioenc = Base64EncodePipe(io)
    write(ioenc, c.content)
    close(ioenc)
    
    write(io, "\" />")
end
```

上のコードで、 GIF アニメも再生されることを確認できた。

これと同じ方法で、 BMP / GIF / MP4 の表示が可能となる。

--
Base64 の参考ページ
[base64ってなんぞ？？理解のために実装してみた](https://qiita.com/PlanetMeron/items/2905e2d0aa7fe46a36d4)

バイナリ 6 bit を１文字 1 byte = 8 bit に伸長するので、データサイズは 33% 増加するようだ。


# 画像を変換してから表示する

最後に、色の配列 `AbstractMatrix{<:Colorant}` などの、ブラウザで直接表示できない形式の画像を、適当な形式に変換して表示しよう。

既存のパッケージでは ImageShow.jl に相当する。
https://github.com/JuliaImages/ImageShow.jl

色の配列ならば、 `FileIO.save` で好きな形式に変換[^imagemagick]できる。

下のコードでは、色の配列を BMP 形式に変換し、 Base64 エンコードしてから HTML タグで囲んで出力している。

```julia
using FileIO

function Base.show(io::IO, ::MIME"text/html", c::ImageContainer{:jlc})
    write(io, "<img src=\"data:image/bmp;base64,")
    
    ioenc = Base64EncodePipe(io)
    save(Stream(format"BMP", ioenc), c.content) 
    close(ioenc)
    
    write(io, "\" />")
end
```

この流れを応用すれば、ブラウザが表示・再生できる形式に変換できるならば、どんなデータでも Jupyter 上で取り扱うことができるだろう。

[^imagemagick]: 内部で ImageMagick を呼び出している


## 補足：出力フォーマットの選択

先ほどのコードで、出力フォーマットを BMP にした理由を説明する。

下のベンチマークで、フォーマットによる処理時間を計測した。

```julia
using ColorTypes

io = IOBuffer()
c = ImageContainer{:jlc}(rand(RGB, 1000, 1000))
@time Base.show(io, MIME("text/html"), c)
```

このベンチマークでは、ランダムな画素によって構成されている画像で計測した。
最も圧縮しにくいケースではあるが、例えばイジングモデルの表示など、それに近い状況は多い。

第一択と考えていた PNG は、無圧縮の形式に対して１０倍以上遅い。
第二択の TIFF は、 Safari 以外の主要なブラウザが対応していなかった。

最近では環境依存などの問題も少なそうなので、 BMP を採用した。

| フォーマット | 処理時間 |
| :----------: | :------: |
|     BMP      | 0.07 sec |
|     TIFF     | 0.07 sec |
|     PNG      | 0.95 sec |


Web ブラウザの画像フォーマット対応状況
https://en.wikipedia.org/wiki/Comparison_of_web_browsers#Image_format_support