Guide: Optimized process for video to GIF conversion

Questions and postings pertaining to the usage of ImageMagick regardless of the interface. This includes the command-line utilities, as well as the C and C++ APIs. Usage questions are like "How do I use ImageMagick to create drop shadows?".
Post Reply
SineSwiper
Posts: 23
Joined: 2011-07-19T04:20:20-07:00
Authentication code: 8675308

Guide: Optimized process for video to GIF conversion

Post by SineSwiper » 2012-01-16T20:47:00-07:00

Okay, I'm pretty much done with my research and done tweaking my code. This method of video to GIF will achieve both the best quality and good size compression. Obviously, figures can be adjusted to move towards one or the other, but I'll cover some of that as well. On average, a 8fps GIF at a width of 300 will use about 150-200KB/s at these settings, and the quality is excellent.

First, the movie rip. IM has serious problems trying to grab frames from a large movie file. However, this is because it simply calls ffmpeg to grab the entire movie as an uncompressed file. You can just modify the middleman step yourself by using ffmpeg to output a PAM file:

Code: Select all

ffmpeg -ss 00:12:30 -i movie.wmv -r 8 -vframes 32 -vbsf remove_extra -an -vcodec pam -f rawvideo -y anim.pam
This grabs a 4-second section of the movie at the 12m30s mark, using 8 frames a sec. Feel free to adjust the parameters as necessary. FFMPEG will convert YUV to RGB if needed. If you get any errors, like "no keyframe", you will likely want to adjust the section mark to prevent artifacting (grey square distortions). Even at 32 frames, this will end up being around 50-100MBs for a larger-sized video.

Second, you will probably want to resize the frames to a smaller size. Something like 1280x768 ain't going to work on animated GIF. How you resize it is critically important, so I'll tell you the best method:

Code: Select all

convert anim.pam -depth 32 -colorspace YCC -resize 300x169! -colorspace Lab -identify anim.miff
Okay, this converts the frames to a 32-bit YCC colorspace, resizes the frames, and turns it to a Lab colorspace.

Why YCC? After a bunch of research on all of the different colorspaces, I've found that YCC provides the lowest color usage ratio for dither operations. This means that the ordered dither can achieve much higher posterization levels than with other colorspaces. Higher posterization levels means much more colors to choose from in the dither, and more importantly, a higher luminance:hue ratio. (More on that in a bit.)

Why Lab? Lab is the preferred colorspace for color quantization and dithering because it is perceptually uniform, ie: a change of the same amount in a color value should produce a change of about the same visual importance to the human eye. Compare this to RGB, where G=255 looks the brightest and B=255 looks not as bright (because our eyes can see green "better"). Since a dither operation is about picking colors with a similar value to each other, Lab dithers will blend in much better.

Why MIFF? Because IM's own format is the only one that would be able to store an animation of frames beyond 256 colors. Remember, we still need to dither before we get that low in the color count.

The hard part will be the ordered-dither guessing process. Since I'm doing this via code, I'll put some of this in a pseudo-code format. Note that I have various shortcuts in place to make the process faster.

A few things to know before we get into it, though. First, the L in Lab is luminance and the AB are the colors. (They call AB "color-opponent dimensions", but the important thing is that AB holds all of the hue/saturation type values and that you really can't separate AB without getting really weird results.) Second, the human eye can see differences in luminance much better than differences in color. So, while the color levels should still be an a certain minimum, the luminance levels should be as high as we can get them.

The bare minimum dither should be 15,11,11. That gives 15 levels of luminance and 11*11=121 different colors, plus the dithering itself which "fakes" other hues. For my uses, I've found that AB levels from 11 to 13 are best. Anything higher than that is wasting what little color space GIF has, instead of favoring luminance more. The goal here, like it was with the plane.avi demo in the IM guide, is to get 255 colors (saving one for transparency).

Also, I'm using three ordered patterns: o8x8, o4x4, and o2x2. The former is the best, but the latter can work well if you need to use less byte size.

Code: Select all

thresholds = (o8x8 o4x4 o2x2)
od = shift thresholds  // in other words, od = o8x8
ab_max = 13
for l in (15 .. 100) {
   for ab in (11 .. ab_max) {
      lvl_str = join ',', (od l ab ab)
      ttl_colors = l * ab * ab

      cratio = min_cratio{od+','+ab} || min_cratio{od+',11'} || min_cratio{'o8x8,11'}
      if (ttl_colors >= cratio * 250) {  // ratio is too low to hit 255 colors, so don't waste time with the query
         open ID "| convert anim.miff -ordered-dither "+lvl_str+" -depth 8 -colorspace sRGB -append -format %k info:")
         num_colors = int(<ID>)
         close(ID)
         
         cratio = ttl_colors / num_colors
         min_cratio{od+','+ab} = min(cratio, min_cratio{od+','+ab} || 99999)
      }
      else num_colors = int(ttl_colors / cratio)
      
      if      (num_colors > 255) { ab_max = ab-1; last; }  // too many colors; bounce out and don't use that AB level again
      else if (num_colors < 200) ab = 13  // too shallow to use; set to AB=13 when adding the levels
      
      // add all of the levels that work, since lower thresolds will have lower # of colors
      for new_od in (od, thresholds) {
         lvl_str = join ',', (new_od l ab ab)
         push levels, lvl_str
      }
      
      got_result = 1
      last if (num_colors >= 250)  // close call; don't push it...
   }
   if not got_result {
      od = shift thresholds
      last if not od
      l--; ab_max = 13;
   }
}
You can do this manually by just experimenting with the convert line:

Code: Select all

convert anim.miff -ordered-dither o8x8,15,11,11 -depth 8 -colorspace sRGB -append -format %k info:
Just adjust the dither levels and the threshold pattern to your liking and try to hit 255 colors.

Why sRGB? Well, first, the ordered-dither is currently playing around in the Lab colorspace, so we need to convert it back to a RGB-like format for the final animation. Second, sRGB is especially made for monitors, cameras, smart phones, and other digital equipment. Since the GIF is going to be shown on a monitor, this is the best colorspace to use. Also, sRGB will subtract out the really dark ranges that RGB has, since there tends to be a lot of "unusable" colors in that range. (Can you really see something like B=10?) This means more posterization levels and more "usable" colors.

The final convert line is:

Code: Select all

convert anim.miff -ordered-dither o8x8,25,12,12 -depth 8 -colorspace sRGB +dither +remap -coalesce -deconstruct -layers RemoveDups -layers Optimize -delay 1x9 -identify anim.gif
This will produce the final GIF. You'll notice in the code above that I store the level strings in an array. This is for reducing the dither level if I need to, to make the GIF use up less space. For example, if this GIF is too big, I can either steadily drop down the levels, or I can go to something like o2x2 for a bigger size drop.

Notice that this command will remap the colors to a global color table. If you are stitching different scenes together, you will want to process the GIFs separately and use a simple "convert *.gif combined.gif" command to bind them together. This will maintain a global color table for each individual GIF/scene, but not a shared one among the whole file.

Post Reply