I was tasked with reducing the amount of time it took to generate an animated GIF as close as possible to 30 frames in length at 150 pixels width. Most of the sequences we generate are under 1000 frames. We had a 15,000 frame sequence and our render nodes were taking 17 minutes to produce this ~30-frame GIF, which is unacceptably slow.
We were using ffmpeg as a demuxer and piping to imagemagick. Through several hours of experimentation, I arrived at the following conclusions:
The number of input frames you ask ffmpeg to process is BY FAR the most impactful input in terms of execution speed. If using the concat demuxer to skip input frames is an option, this will make the biggest performance difference. By taking every 5th frame, I was able to reduce total computation time to 1 minute 45 seconds with high-quality lanczos rescaling and per-frame palette computation. Generating our 30-frame preview thumbnail now takes under 1 second.
The rescaling algorithm was the next largest performance impactor (but a far distant second). Using fast_bilinear instead of lanczos saved 150 seconds of compute time over all 15,000 frames.
The least impactful variable was palette computation, and this varied with rescale algorithm. Over 15,000 frames using lanczos, we saved around 17 seconds of execution time if we eliminated palette computation. Using fast_bilinear, we saved around 75 seconds of execution time.
Because the rescaling algorithm and palette computation were negligible, we ended up keeping them at the highest quality. We have reduced our computation time from 17 minutes to under 1 second mostly by telling ffmpeg to skip reading input files.
KEY TAKEAWAY: SKIPPING INPUT FRAMES vs SKIPPING OUTPUT FRAMES
The reason our process was taking so long is that frame dropping does not help execution time when using the image2 demuxer. If you muck with the -r
flag and the fps
filter, you will affect the number of frames which appear in the final GIF, but ffmpeg appears to still do something with all 15,000 input frames.
The only way I could find to have ffmpeg skip input frames is by using the concat
demuxer.
Here is how I now generate high-quality animated GIF thumbnails on my dev machine in under 1 second by skipping input frames:
# create text file which describes the ~30 input frames we want ffmpeg to process
seq -f "file 'left_frames.%04g.jpg'" 10000 500 25000 > tmp.txt
# generate the animated gif using ffmpeg only
ffmpeg -f concat -i tmp.txt -filter_complex "scale=150:-1:flags=lanczos,split=2 [a][b]; [a] palettegen [pal]; [b] fifo [b]; [b] [pal] paletteuse" output.gif
1Your command is syntactically incorrect; you're missing a
[b]
label. Either way, using the palette is about 200⨉ slower judging from a quick test. Not using it at all is not an option for you? – slhck – 2018-08-08T09:00:26.213@slhck I am finding the ffmpeg docs around this stuff pretty tough to grok, but I came up with this for eliminating the palette:
ffmpeg -y -r 24 -f image2 -start_number 1 -i "frames.%04d.jpg" -filter:v "scale=150:-1:flags=fast_bilinear" output.gif
The two problems with this: 1) It only saved 13 seconds (59 seconds instead of 72), and 2) I'm not 100% sure this command eliminates palette computation altogether (only that the palette computation I was specifying before is not included). – Rjak – 2018-08-08T21:58:44.3771Yes, that's basically what I meant. Sorry for not being so explicit, I thought you knew what the filter command you were using was doing. In my tests, leaving out the palette altogether actually sped up the command significantly. What led you to the original conclusion that the actual encoding only took 5 seconds? – slhck – 2018-08-09T07:08:15.880
Observation of the output, which is probably not a great indicator. When I run the command without palette generation, the "current frame" indicator spins up immediately and we start processing frames. When I run it with palette generation, the current frame sits at 0 until about 5 seconds before the end of processing, then it spins all the way through the frames very quickly. – Rjak – 2018-08-09T22:24:20.343
1Yes, that's also what I'm observing, but it's much faster then. But I guess it depends on the length of the video. To be honest, I don't know if there's any faster way to generate the GIF than just doing
ffmpeg -i <input> <scale> <output.gif>
. – slhck – 2018-08-10T06:33:38.323Yeah it sure seems like I'm maxing it out. I am now working on another method where we skip frames if the clip is long enough (e.g. if the clip is 15,000 frames we only use every 100th frame so that we end up with an animated GIF with 150 frames in it). The hurdle there is that I am in fact ending up with a 150 frame GIF, but it appears to be doing computation on all the frames I want to skip as well so it is taking the same amount of time to complete! Getting this with
-filter_complex "select='not(mod(n\,100))',scale=150:-1:flags=fast_bilinear"
– Rjak – 2018-08-13T04:08:18.940