ポイント
- Jupyter 上でどうやって画像を表示するのか?
- Jupyter が対応していないフォーマットの画像を表示するには?
- 値型やメタプログラミングは、どういう場面で用いられるのか?
この記事の内容を実装したパッケージ
https://github.com/Lirimy/ImageContainers.jl
はじめに
この記事が想定している状況は、例えば、 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}
# 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
などと次々に型をつくっていくのも不毛だろう。
これを解決するのが値型である。
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
ここでは、画像フォーマットに応じたシンボルによって1、 ImageContainer
は別の型だとみなされる。
したがって、多重ディスパッチが効くため、表示させたいすべての画像フォーマットに対して Base.show
を繰り返し記述すれば、目的は達せられることになる。
メタ・プログラミング!
さて前節は、似たようなことを繰り返し記述するのが面倒だ、という話であった。
これこそメタプログラミングの出番であろう。
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
はシングルトン2で、評価に備えて@MIME_str
が用意されている
補足:シンボルの評価を逃れるには?
先の例で、 フォーマットを表す変数 fmt
には、中にシンボルが入っている。
このため、$fmt
が評価されるとエラーが出てしまう。
評価後にシンボルを残すには、どうすればよいだろうか?
# 評価後に ImageContainer{:png} を得たい
fmt = :png
@eval ImageContainer{$fmt}
#ERROR: UndefVarError: png not defined
方法0:QuoteNode
コメントで @antimon2 さんに教えていただきました。
@eval ImageContainer{$(QuoteNode(fmt))}
#ImageContainer{:png}
方法1:配列に隠す
配列の要素に隠し、配列を評価したあとで、中から要素を取り出せばよい。
@eval ImageContainer{$[fmt]...}
#ImageContainer{:png}
参考:Metaprogramming in JuliaLang 10 / 38
方法2:実体をつくる
実はこちらの方を先に思いつき、後から方法1が見つかった。
結局使わなかったが、もったいないので共有して供養したい。
dummy = ImageContainer{fmt}(UInt8[])
@eval typeof($dummy)
#ImageContainer{:png}
HTML に画像を埋め込む
ここまでで、 PNG / SVG / JPEG の表示は実装できた。
これができたのは、実は IJulia.jl がそれらの MIME タイプを素直に受け取ってくれるためである。
それでは、 IJulia.jl が対応していない画像フォーマットは表示できないのだろうか。
別にそんなことはなく、 MIME("text/html")
にして、HTML タグを介して出力すれば事足りる。
画像データの渡し方
次に検討すべきなのは、画像データの送り方である。
方法は2つある。
- 一時ファイルに書き出し、リンクする
- Base64 エンコードして直接出力する
1 はよく使われる方法で、実際に Plots.jl/animation.jl ではそのように実装されている。
いろいろ考えた結果、今回は 2 を採用する。
2 を採用する理由としては、 Jupyter Notebook のファイルに出力画像が埋め込まれることだろうか。
画像の管理が不要になり、 Gist などで Notebook の共有が気軽にできるようになる。
逆に Notebook のファイルサイズが増えたり、バージョン管理が重くなったりするデメリットもある。
(セミコロンで出力を抑制すれば防げるが)
Base64 エンコード
Base64 とは、バイナリデータをアルファベットなどに対応させ、テキストとして取り扱えるようにするエンコード方式である。
今回は、画像データを Base64 エンコードして、 HTML タグ内に
<img src="data:image/gif;base64,R0lGODdhgALgAeYAAP...isQADs=" />
などとして埋め込む。
Base64.Base64EncodePipe
を通すとエンコードしてくれるので、そのまま実装しよう。
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ってなんぞ??理解のために実装してみた
バイナリ 6 bit を1文字 1 byte = 8 bit に伸長するので、データサイズは 33% 増加するようだ。
画像を変換してから表示する
最後に、色の配列 AbstractMatrix{<:Colorant}
などの、ブラウザで直接表示できない形式の画像を、適当な形式に変換して表示しよう。
既存のパッケージでは ImageShow.jl に相当する。
https://github.com/JuliaImages/ImageShow.jl
色の配列ならば、 FileIO.save
で好きな形式に変換3できる。
下のコードでは、色の配列を BMP 形式に変換し、 Base64 エンコードしてから HTML タグで囲んで出力している。
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 上で取り扱うことができるだろう。
補足:出力フォーマットの選択
先ほどのコードで、出力フォーマットを BMP にした理由を説明する。
下のベンチマークで、フォーマットによる処理時間を計測した。
using ColorTypes
io = IOBuffer()
c = ImageContainer{:jlc}(rand(RGB, 1000, 1000))
@time Base.show(io, MIME("text/html"), c)
このベンチマークでは、ランダムな画素によって構成されている画像で計測した。
最も圧縮しにくいケースではあるが、例えばイジングモデルの表示など、それに近い状況は多い。
第一択と考えていた PNG は、無圧縮の形式に対して10倍以上遅い。
第二択の 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