Help us understand the problem. What is going on with this article?

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

ポイント

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

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

example.png

はじめに

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

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

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

Jupyter での画像表示

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

これは、Julia の特徴である多重ディスパッチを利用して、独自の型に対する Base.show 関数を定義すればよい。

# 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

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

https://docs.julialang.org/en/v1/base/io-network/#Base.show-Tuple{Any,Any,Any}

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

前節では、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

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

したがって、多重ディスパッチが効くため、表示させたいすべての画像フォーマットに対して 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}

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

方法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つある。

  1. 一時ファイルに書き出し、リンクする
  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% 増加するようだ。

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

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

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

色の配列 Matrix{<:Color} ならば、 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


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

  2. フィールドを持たない複合型で、インスタンスが1つしかない 

  3. 内部で ImageMagick を呼び出している 

Lirimy
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした