はじめに
アニメーションでやってみたいことがあるので、既存のコードでどのような実装がなされているのかを調査する。
具体的には、Plots.jl1 の animation.jl を読んでいく。
https://github.com/JuliaPlots/Plots.jl/blob/master/src/animation.jl
まずは概要から
Animation の使われ方
一般的な使われ方
anim = @animate for i in 1:10
plot(t->sinpi(t+i/5), range(0, 2, length=100))
end
#gif(anim, "sin.gif", fps=10)
マクロを展開すると?
意味的には次のコードになる。
anim = Animation()
for i in 1:10
plt = plot(t->sinpi(t+i/5), range(0, 2, length=100))
frame(anim, plt)
end
より正確には、 @animate
の部分を @macroexpand
するとマクロ展開後のコードが得られるので確認しよう。
# gensym を使って変数名を生成した結果、 var"##tag#番号" という変数が得られる?
var"##anim#576" = Animation()
global var"##counter#577" = 1
for i = 1:10
plot((t->begin
sinpi(t + i / 5)
end), range(0, 2, length=100))
# 例えば 10 フレームごとにプロットする場合は var"##counter#577" を使って条件分岐する
if true
frame(var"##anim#576")
end
global var"##counter#577" += 1
end
var"##anim#576"
plt
が明示されていないが、現在のプロットを保持する変数 CURRENT_PLOT
が暗黙的に使われる(current()
を通して呼び出される)。
https://github.com/JuliaPlots/Plots.jl/blob/master/src/plot.jl
処理の流れ
- 連番 PNG のパスを記憶するために anim を作成する
- フレームごとに plot から PNG を出力する
- ffmpeg を呼び出して、連番 PNG から mp4 に変換する
主な関数をもっと詳しく
Animation
連番 PNG を管理する
"Represents an animation object"
struct Animation
dir::String
frames::Vector{String}
end
function Animation()
tmpdir = convert(String, mktempdir())
Animation(tmpdir, String[])
end
- 一時ディレクトリを作成する
- 一時ディレクトリのパスとフレーム数を記憶する構造体を返す
frames::Vector{String}
がフレーム数を記憶するわけなのだが、なぜこれが単なる整数ではなく、文字列の配列になっているかは疑問に思った。試してみると、(デフォルトで immutable となる)構造体だと内部変数を変更できないが、内部配列ならその要素を変更できるようなのである。immutable の制限を回避するためにこのような方法をとっているのだろうか。dir
は変更されないほうがいいのは理解できるし、まあそういうものかととりあえずは納得した。
frame
PNG を1フレーム出力する
"""
frame(animation[, plot])
Add a plot (the current plot if not specified) to an existing animation
"""
function frame(anim::Animation, plt::P=current()) where P<:AbstractPlot
i = length(anim.frames) + 1
filename = @sprintf("%06d.png", i)
png(plt, joinpath(anim.dir, filename))
push!(anim.frames, filename)
end
- anim と plt を受け取る
- anim から現在のフレーム数を取得する
- 現在のフレーム数を6桁まで0埋めして PNG のファイル名にする(000001.png など)
- Plots.png で plt を PNG に出力する
- anim のフレーム数を加算する
ここで見ればわかるように、 Animation.frames
は連番 PNG のファイル名の配列になっている。
mp4
連番 PNG から mp4 を作成する
ごちゃごちゃしているが、本質的にはこの2行に集約される。fn
は動画のファイル名。
run(`ffmpeg -v 0 -framerate $fps -loop $loop -i $(animdir)/%06d.png -pix_fmt yuv420p -y $fn`)
AnimatedGif(fn)
ffmpeg に PNG 連番ファイルを渡して mp4 を作成している。
その後で返ってくる AnimatedGif
は後述。
Base.show
"Wraps the location of an animated gif so that it can be displayed"
struct AnimatedGif
filename::String
end
# write out html to view the gif... note the rand call which is a hack so the image doesn't get cached
function Base.show(io::IO, ::MIME"text/html", agif::AnimatedGif)
ext = file_extension(agif.filename)
write(io, if ext == "gif"
"<img src=\"$(relpath(agif.filename))?$(rand())>\" />"
elseif ext in ("mov", "mp4")
"<video controls><source src=\"$(relpath(agif.filename))?$(rand())>\" type=\"video/$ext\"></video>"
else
error("Cannot show animation with extension $ext: $agif")
end)
end
mp4 作成の後に返される AnimatedGif
は、作成した動画のパスを保持している。
Jupyter などでオブジェクトが呼ばれると、その型に合った Base.show
が呼ばれる。上のコードはその時の動作を記述している。要するに、 mp4 関数が呼ばれると作成した動画を Jupyter 上に表示するような HTML タグを出力する。
Julia / Jupyter での画像表示を実装する
https://qiita.com/Lirimy/items/b5604b41247ba8ca7a00
アドレスに乱数を付加しているのは、キャッシュにアクセスさせず、毎回直に動画ファイルを読み込みにいかせるためのテクニックのようだ。
-
Copyright (c) 2015: Thomas Breloff. ↩