Create animation from images previously created with Javis or Luxor?

Hello All,
as far as I understand creating an animation with Javis (or Luxor) always starts from the “outside” with specifying the number of frames that shall constitute the resulting animation and then calling a drawing function for each of these frames.

But what if I don’t know the number of frames beforehand because I would like to visualize some kind of simulation with “unknown” outcome (i.e. number of frames)?

I would like to draw one image per simulation cycle and create an animation once my simulation finished at some stop condition.

Can I facilitate the features of Javis or Luxor for this (e.g. automatically creating individual filenames for every image and storing them at a specified location on disc etc.)?

I’ve seen lots of JuliaCon presentations showing such visualizations but didn’t find any recipe so far.

I don’t have any requirements on the file format.

I’m not too sure what you want, but perhaps you can use snapshots. You can save a snapshot of the current drawing only when, for example, some condition is satisfied.

This example saves a snapshot of the drawing whenever a random point lies inside a circle - so you’ll never see any points lying outside…

using Luxor

function run_it()
    Drawing(250, 250, :rec)
    background("white")
    origin()
    radius = 50
    pt1 = Point(0, 0)
    sethue("blue")
    circle(pt1, radius, :stroke)
    drawing_number = 1
    for i in 1:100
        pt2 = rand(BoundingBox())
        if distance(pt1, pt2) < radius
            circle(pt2, 5, :fill)
            snapshot(fname="/tmp/temp-$(drawing_number).png")
            drawing_number += 1
            @info "we saved a snapshot when i was $(i)"
        end
    end
    finish()
    preview()
end 

run_it()

Thanks @cormullion, that’s somewhat half of what I would like to have.
I modified it to start each drawing “from scratch” next to the Julia script.

And I’m not sure what finish() and preview() are doing for me. Moving them into the for loop, what I somehow would expect to be necessary, gives a compilation error.

Then I added the second half of the “movie making” from the example in the VideoIO.jl documentation.
Unfortunately it’s not as easy as it looked there. I don’t understand the error message occurring. There’s some problem with the do syntax for writing the video file.
But that might be for someone else to answer.

using Luxor
using VideoIO, ProgressMeter

function run_it()
    drawing_number = 1
    for i in 1:100
        Drawing(250, 250, :rec)
        background("white")
        origin()
        radius = 50
        pt1 = Point(0, 0)
        sethue("blue")
        circle(pt1, radius, :stroke)
        pt2 = rand(BoundingBox())
        if distance(pt1, pt2) < radius
            circle(pt2, 5, :fill)
            snapshot(fname=string(@__DIR__, "/tmp/$(drawing_number).png"))
            drawing_number += 1
            @info "we saved a snapshot when i was $(i)"
        end
    end
    finish()
    preview()
end

run_it()

dir = string(@__DIR__, "/tmp") #path to directory holding images
imgnames = filter(x->occursin(".png",x), readdir(dir)) # Populate list of all .pngs
intstrings =  map(x->split(x,".")[1], imgnames) # Extract index from filenames
p = sortperm(parse.(Int, intstrings)) #sort files numerically
imgnames = imgnames[p]

encoder_options = (crf=23, preset="medium")

firstimg = VideoIO.load(joinpath(dir, imgnames[1]))
open_video_out("video.mp4", firstimg, framerate=5, encoder_options=encoder_options) do writer
    @showprogress "Encoding video frames.." for i in eachindex(imgnames)
        img = VideoIO.load(joinpath(dir, imgnames[i]))
        write(writer, img)
    end
end

Making the frames

My interpretation of your original post is that you wanted more control of when, and how many, frames were output from some kind of ongoing simulation. I thought the ‘snapshot’ feature was probably the way to go. Here’s a more complete version of my first example:

using Luxor

function run_it()
    Drawing(300, 300, :rec)
    origin()
    background("black")
    redcircle, greencircle, purplecircle = 
        ngon(O + (0, 10), 70, 3, π / 6, vertices=true)
    radius = 40
    drawing_number = 1
    for i in 1:1000
        frame_worthy = false
        pt2 = rand(BoundingBox())
        if distance(pt2, redcircle) < radius
            sethue("red")
            frame_worthy = true
        elseif distance(pt2, greencircle) < radius
            sethue("green")
            frame_worthy = true
        elseif distance(pt2, purplecircle) < radius
            sethue("purple")
            frame_worthy = true
        else
            frame_worthy = false
        end
        if frame_worthy
            circle(pt2, 4, :fill)
            framename = string("/tmp/", 
                  lpad(drawing_number, 10, "0"), ".png")
            snapshot(fname=framename)
            drawing_number += 1
        end 
    end
    finish()
    Luxor.FFMPEG.ffmpeg_exe(`
        -v 0
        -r 24  
        -f image2 
        -i /tmp/%10d.png -c:v libx264 -pix_fmt yuv420p 
        -y /tmp/dotty.mp4
        `);
end

run_it()

which makes this:

(GIF version if Discourse doesn’t like the video format)

dotty

and this approach lets you handle the “what if I don’t know the number of frames beforehand”, the “unknown outcome”, and “some stop condition” requirements you mentioned. Replace the for with a while - you could run it for weeks until some condition is met…

However, your example code initializes a new empty drawing on each iteration, so it’s not using this incremental snapshot feature.

And I’m not sure what finish() and preview() are doing for me. Moving them into the for loop, what I somehow would expect to be necessary, gives a compilation error.

With only a single drawing, you need just one finish() (to tidy up) and preview() (to see the finished drawing). I couldn’t see any compilation errors if you made a new drawing in every loop (preview() would obviously not be useful). But perhaps if you’re leaving a few thousand drawings open and unattended you might eventually see some other errors…

Making the video

I don’t know anything about VideoIO.jl, since Luxor predates it by a few years, and I’ve stuck with FFMPEG. :slight_smile: You should probably open a new issue/post to get help with that package from someone who knows more.

You don’t show the errors you see, but perhaps they’re the same as mine:

ERROR: MethodError: no method matching VideoIO.VideoWriter(::String, ::Vector{…}; encoder_options::@NamedTuple{…}, framerate::Int64)

Closest candidates are:
  VideoIO.VideoWriter(::AbstractString, ::Type{T}, ::Tuple{Integer, Integer}; codec_name, framerate, scanline_major, container_options, container_private_options, encoder_options, encoder_private_options, swscale_options, target_pix_fmt, pix_fmt_loss_flags, input_colorspace_details, allow_vio_gray_transform, sws_color_options, thread_count) where T
   @ VideoIO ~/.julia/packages/VideoIO/ZM7RD/src/encoding.jl:234
  VideoIO.VideoWriter(::Any, ::AbstractMatrix{T}; kwargs...) where T
   @ VideoIO ~/.julia/packages/VideoIO/ZM7RD/src/encoding.jl:379

The second argument should apparently be a Type or a Matrix:

julia-1.10> firstimg
1-element Vector{PermutedDimsArray{ColorTypes.RGB{FixedPointNumbers.N0f8}, 2, (2, 1), (2, 1), Matrix{ColorTypes.RGB{FixedPointNumbers.N0f8}}}}:
 [RGB{N0f8}(0.0,0.0,0.0) RGB{N0f8}(0.0,0.0,0.0) … RGB{N0f8}(0.0,0.0,0.0) RGB{N0f8}(0.0,0.0,0.0); RGB{N0f8}(0.0,0.0,0.0) RGB{N0f8}(0.0,0.0,0.0) … RGB{N0f8}(0.0,0.0,0.0) RGB{N0f8}(0.0,0.0,0.0); … ; RGB{N0f8}(0.0,0.0,0.0) RGB{N0f8}(0.0,0.0,0.0) … RGB{N0f8}(0.0,0.0,0.0) RGB{N0f8}(0.0,0.0,0.0); RGB{N0f8}(0.0,0.0,0.0) RGB{N0f8}(0.0,0.0,0.0) … RGB{N0f8}(0.0,0.0,0.0) RGB{N0f8}(0.0,0.0,0.0)]

VideoIO.load loads a 'vector of image arrays`, so perhaps you need to pass just the first element. I’m just guessing now, and video stuff is complicated… :slight_smile:

Thank you very much!
Especially for the lpad() trick.
How on earth one shall know all these little helper functions from the beginning?

I’ve created a new post tagged with “videoio” for the other problem. You were right about the nature of the error message.