Let's say we have a list of file names in the correct order like this:
frames=(
"5K1OCNKToUu.png" "kJuKFQS0Fgnp.png" "00v4U4JTyUn.png" "3sg9sDwPZoX.png"
"1KsuEk9mboa9.png" "qNrI8zlRBmW.png" "MOvSca3wlPsP.png" "5rXcxfGQXunY.png"
"hjruIcoN0aTn.jpg" "OhRttnWtKy.png" "e2Qj8jCixc.png" "Uze2H7vzrt4.png"
"n14qhmjiBW3.png" "ZDMvY4g1hzgS.png" "ibnb7MxELyGp.png" "9c8QGWmBEDNg.png"
"STQT0t7oqPEK.png" "jI7UvpbLDWPc.png" "6clazeaUAJHv.png" "ylJ40r9uMK9d.png"
"RICq5KV00P6.png" "zjCLrappFMPq.png" "TJQTDv313KBo.png" "Gu3pLpWylsuo.png"
"Ksym4SB6VYNv.png" "rIyj0LJIjBVX.png" "pSUm2J8xYU.png" "Rnsr0H0m7p9A.png"
"x4vVomlOolxt.png" "2W1QURLQUyE8.png" "m3JgtDzQ0VgE.png" "CrjN9TVJKMAU.png"
"IO6pnF83ivqo.png" "hY15nsYDvr4h.png" "1GagDdBM9L7.png"
)
and we want to create an animated GIF out of these with a certain frame rate e.g. FPS=30
.
This can be done like in the other answers by creating symbolic links e.g. named 001.png
to 035.png
and then use:
ffmpeg -i 03%d.png
Another way is to make use of ffmpeg
's concat-feature.
Unfortunately this is a tad easier said than done, because the the feature expects video streams, meaning the images need to be looped as long as needed.
The command to concatenate three images with 500ms inbetween is this:
ffmpeg -f concat -safe 0 -i <(cat <<EOF
file '$(pwd)/1KsuEk9mboa9.png'
duration 0.5
file '$(pwd)/hjruIcoN0aTn.png'
duration 0.5
file '$(pwd)/n14qhmjiBW3.png'
duration 0.5
EOF
) -framerate 2 out.gif
Tested with ffmpeg version 3.0.1-3
.
Explanation:
The concat demuxer expects a file with a list of relative file names prepended by the keyword file
. In order to not clutter the current working directory (or maybe we don't have writing permissions), we use process substition <( ... )
. But this creates a file in /dev/fd/
and if relative file names are used, then this results in error messages like this:
[concat @ 0xc181c0] Impossible to open '/dev/fd/5K1OCNKToUu.png'
That's why the absolute path is given. But absolute paths are not allowed by default, resulting in:
[concat @ 0x17da1c0] Unsafe file name '/home/5K1OCNKToUu.png'
In order to solve that -safe 0
is used.
When trying to specify an input frame rate with -r
ffmpeg -f concat -safe 0 -r 2 -i <(cat <<EOF
file '$(pwd)/1KsuEk9mboa9.png'
file '$(pwd)/hjruIcoN0aTn.png'
file '$(pwd)/n14qhmjiBW3.png'
EOF
) -framerate 2 out.gif
errors like this occur:
[concat @ 0x1458220] DTS -230575710986777 < 0 out of order
DTS -230575710986777, next:40000 st:0 invalid dropping
PTS -230575710986777, next:40000 invalid dropping st:0
The resulting GIF works like expected anyway. Using the duration
option of concat
explicitly solves these warnings/errors.
Note the comment for -r
As an input option, ignore any timestamps stored in the file and
instead generate timestamps assuming constant frame rate fps. This
is not the same as the -framerate option used for some input formats
like image2 or v4l2 (it used to be the same in older versions of
FFmpeg). If in doubt use -framerate instead of the input option -r.
My take on this is that the above warnings from concat, which notify you that it couldn't find a playing length for images, don't result in bad behavior because the framerate is forced after the concat warnings with -r
, which works, but may not be the intended way. I would only use this when manually writing ffmpeg commands, but not in scripts.
Putting together a small script to work with the list of file names specified in the beginning:
function makeGif() {
local targetName=$1; shift
local FPS=$1; shift
# concat doesn't recognize .33 as returned by bc by default
local delay=$(bc <<< "scale=5; x=1/$FPS; if (x<1) print 0; x")
local list
for file in $@; do
list+=$(printf "\nfile '$(pwd)/$file'\nduration $delay")
done
ffmpeg -f concat -safe 0 -i <(cat <<< "$list") -r $FPS "$targetName".gif
#ffmpeg -f concat -safe 0 -i <(cat <<< "$list") -r $FPS -c:v libx264 -crf 5 -pix_fmt yuv420p "$targetName".mkv
}
makeGif out 30 ${frames[@]}
The uncommented line in the code above will give a H264-encoded mkv.
Heterogenous image types
If your image list contains images of different types, e.g. by replacing hjruIcoN0aTn.png
with hjruIcoN0aTn.jpg
in frames
, then this usage of concat
won't work. Images in other formats than the first specified will be dropped and throw errors like:
[png @ 0x1ec8260] Invalid PNG signature 0xFFD8FFE000104A46.
Error while decoding stream #0:0: Invalid data found when processing input
In this case you will have to use the concat filter instead of the simple demuxer. The command with the three files from above will then have to be changed to this:
ffmpeg \
-f image2 -loop 1 -thread_queue_size 4096 -framerate 30 -t 0.5 -i "$(pwd)/1KsuEk9mboa9.png" \
-f image2 -loop 1 -thread_queue_size 4096 -framerate 30 -t 0.5 -i "$(pwd)/hjruIcoN0aTn.jpg" \
-f image2 -loop 1 -thread_queue_size 4096 -framerate 30 -t 0.5 -i "$(pwd)/n14qhmjiBW3.png" \
-filter_complex 'concat=n=3:v=1 [vmerged]' \
-map '[vmerged]' -r 30 out.gif
Explanation:
Each options used is explained really well here. Note that the -framerate
option is specific to the image importer and is at least technically different from -r
, practically in this case they produce the same results.
The -thread_queue_size
seems to be necessary, because we extend one image for a quite long 500ms, resulting in these error messages:
[image2 @ 0x18856e0] Thread message queue blocking; consider raising the thread_queue_size option (current value: 8)
[image2 @ 0x188a100] Thread message queue blocking; consider raising the thread_queue_size option (current value: 8)
[image2 @ 0x188b9e0] Thread message queue blocking; consider raising the thread_queue_size option (current value: 8)
The description of the option gave me reason to think that the overly large delay is at fault:
-thread_queue_size size (input)
This option sets the maximum number of queued packets when reading from the file or device. With low
latency / high rate live streams, packets may be discarded if they are not read in a timely manner;
raising this value can avoid it.
Putting all this in a script again:
function makeGif() {
local targetName=$1; shift
local FPS=$1; shift
local delay=$(bc <<< "scale=5; x=1/$FPS; if (x<1) print 0; x")
local list=()
local nFiles=0
for file in $@; do
list+=( -f image2 -loop 1 -thread_queue_size 4096 -framerate $FPS -t $delay -i "$(pwd)/$file" )
nFiles=$((nFiles+1))
done
ffmpeg ${list[@]} -filter_complex "concat=n=$nFiles:v=1 [vmerged]" -map '[vmerged]' -r $FPS "$targetName".gif
#ffmpeg ${list[@]} -filter_complex "concat=n=$nFiles:v=1 [vmerged]" -map '[vmerged]' -r $FPS -c:v libx264 -crf 5 -pix_fmt yuv420p "$targetName".mkv
}
makeGif out 30 ${frames[@]}
For some very weird reason the above command doesn't work as is !!!. In my case it dropped 33 of the 35 frames specified without a warning or error message:
frame= 2 fps=0.0 q=-0.0 Lsize= 2kB time=00:00:00.07 bitrate= 179.7kbits/s dup=0 drop=33 speed=0.155x
The reason that this is weird is, that it works perfectly like intended for double the delay, i.e. each image being shown for 2 frames, i.e. changing 1/$FPS
to 2/$FPS
:
frame= 70 fps=0.0 q=-0.0 Lsize= 701kB time=00:00:02.33 bitrate=2465.4kbits/s speed= 2.9x
This fact suggests an easy workaround: encode with double the framerate and display each image for 2 frames. But that is wasteful and not favorable anyway.