LoginSignup
18
11

More than 3 years have passed since last update.

Julia におけるアニメーションのしくみ

Last updated at Posted at 2018-05-26

はじめに

アニメーションでやってみたいことがあるので、既存のコードでどのような実装がなされているのかを調査する。

具体的には、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)

sin.gif

マクロを展開すると?

意味的には次のコードになる。

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

処理の流れ

  1. 連番 PNG のパスを記憶するために anim を作成する
  2. フレームごとに plot から PNG を出力する
  3. ffmpeg を呼び出して、連番 PNG から mp4 に変換する

主な関数をもっと詳しく

Animation

連番 PNG を管理する

animation.jl
"Represents an animation object"
struct Animation
    dir::String
    frames::Vector{String}
end

function Animation()
    tmpdir = convert(String, mktempdir())
    Animation(tmpdir, String[])
end
  1. 一時ディレクトリを作成する
  2. 一時ディレクトリのパスとフレーム数を記憶する構造体を返す

frames::Vector{String} がフレーム数を記憶するわけなのだが、なぜこれが単なる整数ではなく、文字列の配列になっているかは疑問に思った。試してみると、(デフォルトで immutable となる)構造体だと内部変数を変更できないが、内部配列ならその要素を変更できるようなのである。immutable の制限を回避するためにこのような方法をとっているのだろうか。dir は変更されないほうがいいのは理解できるし、まあそういうものかととりあえずは納得した。

frame

PNG を1フレーム出力する

animation.jl
"""
    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
  1. anim と plt を受け取る
  2. anim から現在のフレーム数を取得する
  3. 現在のフレーム数を6桁まで0埋めして PNG のファイル名にする(000001.png など)
  4. Plots.png で plt を PNG に出力する
  5. anim のフレーム数を加算する

ここで見ればわかるように、 Animation.frames は連番 PNG のファイル名の配列になっている。

mp4

連番 PNG から mp4 を作成する

ごちゃごちゃしているが、本質的にはこの2行に集約される。fn は動画のファイル名。

animation.jl
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

animation.jl
"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

アドレスに乱数を付加しているのは、キャッシュにアクセスさせず、毎回直に動画ファイルを読み込みにいかせるためのテクニックのようだ。


  1. Copyright (c) 2015: Thomas Breloff. 

18
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
11