Creating High Quality Gifs from Videos with FFmpeg
Recently for my new project Elevator, I wanted to take some video files that demonstrate how the product works and convert them into gifs.
Most online video to gif converters that I have tried have been underwhelming. They usually have strict limits on file size, gif length, and the quality usually seems to come out poor. So I wanted to see if I could experiment with ffmpeg and get some better results.
The initial video file I want to try this with is a pretty large, high quality .mov file titled elevator-demo.mov. It's 27 seconds long and 58.2MB in size. Here's the original video:
Looking at ffmpeg's docs, we can see they give this example as the general format to use:
ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...
So as a starting point, we shoul be able to simply do ffmpeg -i input output
, where input will be my .mov file and output will be the .gif file that I want to create.
So here's the first thing I'm going to try:
ffmpeg -i elevator-demo.mov elevator-demo.gif
Running that successfully creates a new gif from my video input and adds it into the current directory. Here is the gif this created:
On first look, I'm actually quite impressed with the quality of the result. However, there are some pretty clear issues. First of all, it's way bigger than I need (2858x1486!), and the file size is still quite large at 29.2MB (about one-half that of the original video).
On top of this, the pacing of the gif appears to be much slower than the original video. I don't know a ton about video/gif best practices, but presumably if we could reduce the total number of frames I think that might help with both of these issues. Using Imagemagik, we can run this command to actually take a look at the total number of frames in our gif:
identify -format "%n\n" elevator-demo.gif | head -1
Source
The resulting output shows 1,649 frames. 1,649! Again, I don't know that much about gif best practices, but that seems like a lot. So what I'd like to do is try to convert my video to a gif again, but additionally I'd like to reduce the size of the file and either reduce the total number of frames used or modify the fps settings if that's possible.
Looking at stackoverflow, I came across this thread which had this interesting example:
ffmpeg -ss 00:00:00.000 -i yesbuddy.mov -pix_fmt rgb24 -r 10 -s 320x240 -t 00:00:10.000 output.gif
Wow, there's a lot happening in that example - let's see if we can make some sense of it all by looking back at ffmpeg's documentation and breaking down what each command is doing.
-ss: When used as an input option (before -i), seeks in this input file to position. Note that in most formats it is not possible to seek exactly, so ffmpeg will seek to the closest seek point before position.
Okay, so in the above example this explicitly sets the starting point to be the beginning of the video. Not sure we will need that.
-i: input file url
We already know from our first experiment that this is the basic format for setting the path to our input
-pix_fmt: Set pixel format. Use -pix_fmts to show all the supported pixel formats. If the selected pixel format can not be selected, ffmpeg will print a warning and select the best pixel format supported by the encoder.
Okay, so in the above example this would explicitly set the pixel format to be rgb24
.
-r: (fps) Sets the frame rate.
So in this example the frame rate is being set to 10 fps. I can definitely use this to try to tweak the fps and reduce the total # of frames. Ideally this will speed up my gif.
-s: Set frame size.
So in this example it is explicitly setting the size of the new gif to be 320x240. I should be able to use this to reduce the size of my final gif, which will hopefully help to reduce the overall size of the file
-t: (duration) When used as an input option (before -i), limit the duration of data read from the input file. When used as an output option (before an output url), stop writing the output after its duration reaches duration.
It looks like in the above example this would stop writing the output after 10 seconds. Not sure that I need this.
So using this information, we should be able to construct a new command that takes my video and converts it to a gif, while also reducing the file size and setting a specific frame rate. I'm going to try to reduce the dimensions for the gif to be 1072x558. Also, I'm not sure why the stackoverflow example uses 10 fps, but if I'm not mistaken I believe 24 is a fairly standard fps for some videos, so I'm going to try to cut that in half for my gif and use 12 fps.
So with this knowledge, let's try running my command again to conver the video to a gif with these additional parameters:
ffmpeg -i elevator-demo.mov -r 12 -s 1072x558 elevator-demo-smaller.gif
Here's the resulting gif after I ran this command:
The gif still looks pretty good in my opinion, but now we have the added bonus that it looks like we have successfully increased the speed by setting the fps to 12, and we reduced the total file size from 29.2MB to just 2MB! Additionally, if I re-run the command to count the total number of frames in my gif it looks like this also managed to reduce this number down to just 331.
This seems really promising, however to be fair I tried running this same script with different dimensions (943x490) and for some reason it consistently creates a gif much larger than the original video, at 75MB. So it seems like I may need to do some more research into FFmpeg 😅.
Edit #1: After doing a little more research, I came across this post, which led me to this post. After reading that, it looks like we should be able to leverage that approach to improve the color and overall quality of the gif. So let's test it out.
I'm going to create a new script file called gifenc.sh
and then add the following:
#!/bin/sh
palette="/tmp/palette.png"
filters="fps=12,scale=1072:-1:flags=lanczos"
ffmpeg -v warning -i $1 -vf "$filters,palettegen" -y $palette
ffmpeg -v warning -i $1 -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -y $2
Then let's try to create a gif again using this:
./gifenc.sh elevator-demo.mov elevator-demo-hiq.gif
After running this and comparing the gif created with previous gifs, it definitely looks like there's an improvement in the quality. Here's screenshots of the earlier approach and this new approach for comparison:
The previous gifs had a noticably grainy texture, which seems to be almost fully removed in the version created using this script. However, from this initial test, the size of the gif is still quite large, ~45MB in this example.