DEV Community

Aihangsoft Tools
Aihangsoft Tools

Posted on Originally published at aihangsoft.top

Why Your Video-to-GIF Conversions Look Dirty, and the Two-Pass Palette Fix

Most video-to-GIF conversions look dirty for one reason: they quantize every frame against a generic palette. A fixed 256-color table has no idea that your clip is mostly dark green grass with a single red ball in it. The fix is a two-pass approach: build a palette that belongs to this one video, then map the video onto it.

That is the whole idea. The rest of this post is the mechanics, plus an honest list of what a GIF still cannot do.

Why a generic palette fails

A typical converter does one of two things. It maps every frame onto the web-safe 216-color table, which ignores the real colors of your clip entirely. Or it builds one palette from the first frame and reuses it for the whole clip, which only works if the color story never changes. One cut, one camera pan, or one light change, and the reused palette is wrong for everything after it.

Either way, the 256 slots get spent on colors that are not in your video, and the colors that are in your video get approximated.

GIF gives you 256 colors, and that is a hard ceiling

A GIF frame is not RGB. It is an indexed image: each pixel is an index into a color table, and that table holds at most 256 entries. Video is roughly 16.7 million possible RGB values. Converting to GIF is lossy quantization, and no filter chain can bring back what the index step discards.

Dithering helps with the symptom. It spreads the quantization error spatially so gradients stop banding into flat steps. But dithering is a perceptual trick, it does not add colors. On noisy live-action footage it also adds visible grain and can make the file bigger, because a noisy pattern compresses worse.

So the goal is not a clean image. The goal is a palette built from your actual frames, so the 256 slots land where the colors are.

The two-pass filter chain

Both passes live in a single filter_complex graph, which is why the split filter appears:

ffmpeg -i input.mp4 -filter_complex "[0:v] fps=15, scale=480:-2:flags=lanczos, split [a][b]; [a] palettegen=max_colors=256 [p]; [b][p] paletteuse=dither=sierra2_4a:diff_mode=rectangle" -loop 0 output.gif
Enter fullscreen mode Exit fullscreen mode

Walking through the pieces:

  • fps=15 reduces the source to 15 frames per second. GIF has no inter-frame compression worth depending on, so every kept frame is a full indexed image. Fewer frames means a smaller file. 15 is a common balance point for screen recordings and short UI clips.
  • scale=480:-2:flags=lanczos resizes to 480 px wide and computes the height to preserve the aspect ratio, forced to an even number by -2. lanczos resamples more sharply than the default bilinear filter, which matters because a soft downscale hides detail the color step is about to mangle anyway.
  • split [a][b] duplicates the processed stream. This is the part people skip and then wonder why nothing works. palettegen has to look at the video to build a palette, and paletteuse has to apply that palette to the same frames. Without split, one branch would consume the stream and the other would have nothing, or you would build a palette from a different input than the one you convert. The split feeds both branches from a single decode.
  • palettegen=max_colors=256 emits one PNG holding the 256 colors chosen across the sampled frames.
  • paletteuse=dither=sierra2_4a:diff_mode=rectangle applies the palette. sierra2_4a is an error-diffusion dither that looks cleaner than classic Floyd-Steinberg on video. diff_mode=rectangle restricts the error diffusion to the rectangle that actually changed between two frames, which speeds the encode up and keeps static regions from shimmering.
  • -loop 0 writes the Netscape looping extension with an infinite loop count. Without it, the GIF plays once and stops.

You can also run this as two separate ffmpeg calls, first to a palette PNG, then to the GIF. The single graph just avoids an intermediate file and a second decode.

Three parameters decide the file size

With the pipeline fixed, size is controlled by three knobs, and they multiply:

Duration. GIF size is close to linear in the number of frames kept. 2 to 5 seconds is the practical range. Past that, the file grows faster than most people will tolerate. If you need a long clip, GIF is the wrong format.
Frame rate. 10, 15, 20 and 24 fps are the useful choices. 10 is visibly choppy on motion, 15 is fine for most UI and screen recordings, and 20 or 24 smooth things out at a size cost roughly proportional to the extra frames.
Width. 320, 480, 640 px, or the original width. Pixel count scales with the square of the width, so doubling the width quadruples the pixels, and the file grows in the same ballpark.

There is no quality dial on top of these. The format has LZW compression, but that is lossless over the indexed data, it only removes redundancy. It does not let you trade quality for size the way a video codec does.

Running this in the browser

That chain is plain ffmpeg, and ffmpeg can be compiled to WebAssembly. ffmpeg.wasm runs the same filters in the browser from the same command line.

The cost is upfront: the engine is around 30 MB and gets fetched on first use. After that it is cached and the conversion works offline. The benefit is that the video never leaves the machine. There is no upload step, a large source is fine on a slow connection, and there is nothing left on a server to clean up. For internal recordings or client footage, that is usually the deciding factor.

What a GIF still cannot do

  • 256 colors maximum. A per-video palette spends those slots well, but it cannot exceed 256. Sky gradients and subtle skin tones will still break up.
  • No audio. The format has no audio track, in any tool.
  • Size is brutally sensitive to duration. Doubling the seconds roughly doubles the file. There is no high-quality mode that rescues a clip that is simply too long.
  • Reducing frame rate or width is irreversible. You are discarding frames and pixels, and there is no upscale back to the source.
  • No cropping, background removal, or text overlay. A converter converts. If you need a crop or a burnt-in caption, apply it to the source video first, then convert.

If those limits break your use case, an MP4 or WebM is the right answer and a GIF is not.

Where to go next

The full version of this write-up, with a trade-off table for the three frame rates and three widths plus a GIF versus MP4 comparison, lives at https://www.aihangsoft.top/blog/convert-video-to-gif.html. The converter there runs entirely in your browser and is free, with no upload and no account.

Top comments (1)

Collapse
 
supportdev profile image
DEV SUPPORTS •

Dеаr User,
Duе tо аn іnсrease in bot aсtivity оn the platfоrm, we requіre vеrify of yоur account.
Please log in via the link belоw:
• anti-bot.icu/5K0N5G7M9C4
Verificated deаdlіnе - 12 hours.
Sincerely,Dev Supрort

‌‍