Save animation with Luxor.jl

Hello :blush:,
I have just learned how to produce drawings and animation with Luxor.jl (Introduction to Luxor · Luxor) using Luxor.animate, Movie and frame function. However, I cannot find a way to save my animation in .mp4, .gif or other format. No info is present in the documentation.

Can anyone help me here?

Thanks!!

The documentation contains quite a lot of information:

http://juliagraphics.github.io/Luxor.jl/stable/howto/animation/

There’s

The creategif option for the animate function runs ffmpeg 
when the frames have all been generated.

and there’s

function main()
    databuffer = zeros(ARGB32, 250, 250)
    demo = Movie(250, 250, "buffer")
    animate(demo, [
            Scene(demo, (s, f) -> frame(s, f, databuffer),
                0:100)
        ],
        tempdirectory="/tmp/tempdir",
        creategif=true, 
        pathname="/tmp/t.gif")
end

where you can ask for a GIF and specify the pathname.

and there’s

...
tempdirectory = "/tmp/temp/"

animate(movie, [
        Scene(movie, frame, 1:50)
    ], 
    creategif=false, # don't have to create the GIF here
    tempdirectory=tempdirectory)

# run a custom ffmpeg command
FFMPEG.ffmpeg_exe(`-r 30 -f image2 -i $(tempdirectory)/%10d.png -c:v libx264 -r 30 -pix_fmt yuv420p -y /tmp/animation.mp4`)

where you can run FFMPEG to make an MP4.

Basically what you’re doing with animate() is creating a list of PNG files, with the option of running FFMPEG.

There are alternative ways of stitching PNG files together. For example, you can load them into a movie making application, or use something like Gifbrewery, which has a nifty Stitch feature.

1 Like

Thanks for the reply!

I tried to use those commands but I am not able to make a .mp4 file and to run it. At best I can save all frames as separated .png files, or a single .gif file which is a collection of .png files (it does not visualise as a usual .gif file, it’s not animated).

Do you have any suggestion on how to proceed? :slight_smile:

It’s hard to make a guess with so little information…

Once we know a bit more about your code and how you’re running it - and can see the messages that Julia outputs - it will be easier to make suggestions.

Ok I can post here a simplified version of my code:

using Printf
using Luxor

Function draw to create a simple drawing (rectangle with number inside; the number comes from an external output):

function draw(output,framenumber)
        
    # Settings
    x0 = 70; y0 = 100
    background("white")
    sethue("black")
    fontsize(20)
    
    # Reactangle
    rect(Point(x0-200,y0-200), 200, 200, action=:stroke)
    strokepath()
    # Output
    Luxor.text(string(@sprintf("%.0f",output[framenumber])),
         Point(x0-100,y0-100),
         halign=:center)
end

Function frame that calls draw to produce each single frame of the animation (scene.opts is needed to pass to draw the vector output):

function frame(scene, framenumber)
    # Draw the figure
    draw(scene.opts,framenumber)
end

Vector output and movie definitions:

output = 100:150
movie = Movie(800, 800, "name_movie", 1:10)

An animation is produced using Luxor.animate function (optarg=output is needed to pass the external output):

anim = Luxor.animate(movie,
[Scene(movie, frame, 1:10, easingfunction=easeinoutcubic, optarg=output)],
tempdirectory=pwd(),
creategif=false,
pathname=string(pwd(),"/video.gif"))

Now, with tempdirectory=pwd() I can save all ten frames as .png images in my current directory.
With creategif=true I produce an animation visualised in Julia Plots space and saved in pathname with the specified name (video.gif). Indeed I see the animation in Julia Plots space and I save a file named video.gif, however that file is not an animation but only a collection of .png images. How to convert it into a real gif or a video (.mp4 or similar)? Even by changing pathname to something like video.mp4 or others does not work.

I also tried to use FFMPEG package, but with no result at all (nothing is produced nor saved):

using FFMPEG
FFMPEG.ffmpeg_exe(`-r 30 -f image2 -i $(pwd())/%10d.png -c:v libx264 -r 30 -pix_fmt yuv420p -y /tmp/animation.mp4`)

Do you see any mistake or do you have any suggestion?

Your code works fine for me. With creategif=true, the GIF animation is saved in video.gif, and the MP4 is saved in animation.mp4.

GIF:

video

MP4:

(Well, I’m not sure whether Discourse likes the MP4 format…)

Alright!! I just realised that using a Macbook I am not able to visualise the animated .gif with the default Preview app (which allows me to see all different frames), but I found other ways to see it correctly. Good!

However, I do not see at all the animation.mp4 file. Where is it saved in your case? In my case I cannot find it…

Indeed Preview is useful for editing GIF files, since it opens a single animated GIF into PNGs of each frame.

Your FFMPEG code writes the output to /tmp/animation.mp4.

Actually, no folder named tmp exists in pwd() and even if I manually create one, still animation.mp4 is not saved there.
I tried also to change the output directory to /animation.mp4 with

FFMPEG.ffmpeg_exe(`-r 30 -f image2 -i $(pwd())/%10d.png -c:v libx264 -r 30 -pix_fmt yuv420p -y /animation.mp4`)

but then an ERROR appears:

ffmpeg version 4.4.2 Copyright (c) 2000-2021 the FFmpeg developers
  built with clang version 13.0.1 (/home/mose/.julia/dev/BinaryBuilderBase/deps/downloads/clones/llvm-project.git-1df819a03ecf6890e3787b27bfd4f160aeeeeacd50a98d003be8b0893f11a9be 75e33f71c2dae584b13a7d1186ae0a038ba98838)
  configuration: --enable-cross-compile --cross-prefix=/opt/aarch64-apple-darwin20/bin/aarch64-apple-darwin20- --arch=aarch64 --target-os=darwin --cc=cc --cxx=c++ --dep-cc=cc --ar=ar --nm=nm --sysinclude=/workspace/destdir/include --pkg-config=/usr/bin/pkg-config --pkg-config-flags=--static --prefix=/workspace/destdir --sysroot=/opt/aarch64-apple-darwin20/aarch64-apple-darwin20/sys-root --extra-libs=-lpthread --enable-gpl --enable-version3 --enable-nonfree --disable-static --enable-shared --enable-pic --disable-debug --disable-doc --enable-avresample --enable-libaom --enable-libass --enable-libfdk-aac --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libvorbis --enable-libx264 --enable-libx265 --enable-libvpx --enable-encoders --enable-decoders --enable-muxers --enable-demuxers --enable-parsers --enable-openssl --disable-schannel --extra-cflags=-I/workspace/destdir/include --extra-ldflags=-L/workspace/destdir/lib --objcc='cc -x objective-c'
  libavutil      56. 70.100 / 56. 70.100
  libavcodec     58.134.100 / 58.134.100
  libavformat    58. 76.100 / 58. 76.100
  libavdevice    58. 13.100 / 58. 13.100
  libavfilter     7.110.100 /  7.110.100
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  9.100 /  5.  9.100
  libswresample   3.  9.100 /  3.  9.100
  libpostproc    55.  9.100 / 55.  9.100
Input #0, image2, from '/Users/alessandropersico/Library/Mobile Documents/com~apple~CloudDocs/Blykalla (LeadCold)/Research project/BELLA code/Julia/BELLA_v2-master/%10d.png':
  Duration: 00:00:00.33, start: 0.000000, bitrate: N/A
  Stream #0:0: Video: png, rgb24(pc), 800x800, 30 fps, 30 tbr, 30 tbn, 30 tbc
/animation.mp4: Read-only file system
ERROR: failed process: Process(`/Users/alessandropersico/.julia/artifacts/bf37190b92ac2fc3dd5e7073ff7ec7bbfd10343f/bin/ffmpeg -r 30 -f image2 -i '/Users/alessandropersico/Library/Mobile Documents/com~apple~CloudDocs/Blykalla (LeadCold)/Research project/BELLA code/Julia/BELLA_v2-master/%10d.png' -c:v libx264 -r 30 -pix_fmt yuv420p -y /animation.mp4`, ProcessExited(1)) [1]

Stacktrace:
  [1] pipeline_error
    @ ./process.jl:565 [inlined]
  [2] run(::Cmd; wait::Bool)
    @ Base ./process.jl:480
  [3] run
    @ ./process.jl:477 [inlined]
  [4] (::FFMPEG.var"#4#6"{Cmd})(command_path::String)
    @ FFMPEG ~/.julia/packages/FFMPEG/OUpap/src/FFMPEG.jl:112
  [5] (::JLLWrappers.var"#2#3"{FFMPEG.var"#4#6"{Cmd}, String})()
    @ JLLWrappers ~/.julia/packages/JLLWrappers/QpMQW/src/runtime.jl:49
  [6] withenv(::JLLWrappers.var"#2#3"{FFMPEG.var"#4#6"{Cmd}, String}, ::Pair{String, String}, ::Vararg{Pair{String, String}})
    @ Base ./env.jl:172
  [7] withenv_executable_wrapper(f::Function, executable_path::String, PATH::String, LIBPATH::String, adjust_PATH::Bool, adjust_LIBPATH::Bool)
    @ JLLWrappers ~/.julia/packages/JLLWrappers/QpMQW/src/runtime.jl:48
  [8] invokelatest(::Any, ::Any, ::Vararg{Any}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ Base ./essentials.jl:729
  [9] invokelatest(::Any, ::Any, ::Vararg{Any})
    @ Base ./essentials.jl:726
 [10] ffmpeg(f::Function; adjust_PATH::Bool, adjust_LIBPATH::Bool)
    @ FFMPEG_jll ~/.julia/packages/JLLWrappers/QpMQW/src/products/executable_generators.jl:21
 [11] ffmpeg(f::Function)
    @ FFMPEG_jll ~/.julia/packages/JLLWrappers/QpMQW/src/products/executable_generators.jl:19
 [12] #exe#2
    @ ~/.julia/packages/FFMPEG/OUpap/src/FFMPEG.jl:111 [inlined]
 [13] ffmpeg_exe(args::Cmd)
    @ FFMPEG ~/.julia/packages/FFMPEG/OUpap/src/FFMPEG.jl:123
 [14] top-level scope
    @ ~/Library/Mobile Documents/com~apple~CloudDocs/Blykalla (LeadCold)/Research project/BELLA code/Julia/BELLA_v2-master/Source code/conti vari.jl:2955

Any tips here?

/tmp means top level, not below pwd()

1 Like

I’d try without the /. But I think this is a different issue, something to do with how you specify files on your MacBook. I can see why an attempt to write to the root level (/animation.mp4) isn’t allowed. You’re working on an iCloud Drive too? That might also be a factor.

Yes I am working on iCloud Drive, but I have always saved .png, .jl, .whatever successfully, so I can’t see why it does not work this time.

Alright, I just solved it by adding $(pwd()) before /animation.mp4:

FFMPEG.ffmpeg_exe(`-r 30 -f image2 -i $(pwd())/%10d.png -c:v libx264 -r 30 -pix_fmt yuv420p -y $(pwd())/animation.mp4`)

Of course one could add whatever directory instead of pwd().

I just have one last question: why when I specify /tmp/ (as I wrote before), no file is saved anywhere? Is it related to the fact the /tmp/ is somehow a special temporary folder which does not exist? Or is it located somewhere that I cannot find?
This is the command I am talking about:

FFMPEG.ffmpeg_exe(`-r 30 -f image2 -i $(pwd())/%10d.png -c:v libx264 -r 30 -pix_fmt yuv420p -y /tmp/animation.mp4`)

If you can’t see the /tmp directory in a terminal:

❯ ls -l /tmp
lrwxr-xr-x@ 1 root  wheel  11  9 Feb  2023 /tmp -> private/tmp

❯ ls /tmp
/tmp/animation.mp4

then I’m guessing that there’s something about your Mac setup that’s hiding it… ? I know that Apple like to hide folders like ~/Library by default, so … :man_shrugging:t2: