The Ninja Way
a.k.a. Creating animated GIFs with OpenCV, ImageMagick, and Gifsicle
I have these two similar images that I am to use for an undisclosed secret feature, that certainly cannot be derived:


I needed both on the site. However, I feared that people would be confused that they would have to do both, instead of just one or the other.
I'm sure that some UI guru says something like "Don't allow there to be more than one call to action for any given (buzzword that basically means page)".
So I wanted an animation that fades between the two images.
In jQuery, there's a generic $.animate with callbacks. I could do it this way, but I wanted the classic GIF.
The Results
Because I'm impatient too...
Here are a few that I generated:



Nothing too special, really. And I'm not actually going to use them for some other UX reasons. Plus, I'm sure there's some TOS violations in there somewhere.
How I made these is, I hope, however, much more interesting to you, the impatient reader.
Analyzing the Images
If I run ImageMagick's identify program, I find
$ identify tw.png fb.png tw.png PNG 151x24 151x24+0+0 8-bit DirectClass 2.37KB 0.008u 0:00.000 fb.png[1] PNG 154x22 154x22+0+0 8-bit PseudoClass 242c 1.66KB 0.000u 0:00.007
This of course means that they are different sizes and can't be superimposed on top of each other without resizing one of them. Since they are close, I decided resizing wouldn't be too egregious. I resized the twitter icon to the fb dimensions using ImageMagick's convert.
You need to be careful here
$ convert tw.png -resize 154x22 tw1.png
The above command actually won't work ... it will try to keep dimensions. You need to use the !, and escape it.
$ convert tw.png -resize 154\!x22\! tw1.png
Now we rerun identify
$ identify tw1.png fb.png tw1.png PNG 154x22 154x22+0+0 8-bit DirectClass 4.32KB 0.008u 0:00.007 fb.png[1] PNG 154x22 154x22+0+0 8-bit PseudoClass 242c 1.66KB 0.008u 0:00.007
Create the Frames
To fade one image to the other, you want to fade out one while the other fades in. There are many models for fading ... linkjacked image follows

The one I'm going to use is linear or "transition". So when one image is at 20% opacity, the other is at 80%. This one is easy and gets the job done.
So let's say that we want to fade in 5 steps. Here are the opacity levels for each image in each step:
Image1 | Image2 |
100% | 0% |
80% | 20% |
60% | 40% |
40% | 60% |
20% | 80% |
0% | 100% |
At step 2, for instance, when image1 is at 80% and image2 is at 20%, our composite image, the one that we are generating, would basically be0.8 * image1 + 0.2 * image2 and so on.
It's worth noting, that if you want a loop, then you are reversing the process. It's symmetrical.
The "Pivot" Point
(Trying to use that college degree)
We will call the end of the sequence, when image2 is at 100% and image1 is at 0%, the pivot point. This will also be our sequence length.
If we extend the table above to two rounds we'll get the following:
Image1 | Image2 |
100% | 0% |
80% | 20% |
60% | 40% |
40% | 60% |
20% | 80% |
0% | 100% |
20% | 80% |
40% | 60% |
60% | 40% |
80% | 20% |
100% | 0% |
80% | 20% |
60% | 40% |
40% | 60% |
20% | 80% |
0% | 100% |
20% | 80% |
40% | 60% |
60% | 40% |
80% | 20% |
Notice how some step is repeated multiple times. Mathematically, if there are X steps (or here, 5) and you are on step i out of X, then you can expect to see that same image at 2 * X - i - 1The 2 * X is a full round, a fade in and a fade out. The "- i" is because there is a symmetry and the "-1" is because we don't hold the faded-in image for two frames ... this means that the symmetry point is actually in the center of the fully faded in frame with respect to time.
Let me explain this. So at the end of the frame when image2 is fully faded in and the fully faded frame has been on the screen for say, 1 second, you have already started to fade out. Well not really. But if you were to add more frames and wanted to have a smoother transition, say two frames for every one you had before, then you would have started fading out 1 / 2 / 2 times sooner.
WAIT, where did that 1 / 2 / 2 come from? Well let's look at that table again: We're going from
Time | Image1 | Image2 |
0.00 | 20% | 80% |
1.00 | 0% | 100% |
2.00 | 20% | 80% |
Time | Image1 | Image2 |
0.00 | 20% | 80% |
0.50 | 10% | 90% |
1.00 | 0% | 100% |
1.50 | 10% | 90% |
2.00 | 20% | 80% |
Time | Image1 | Image2 |
0.00 | 20% | 80% |
0.25 | 15% | 85% |
0.50 | 10% | 90% |
0.75 | 5% | 95% |
1.00 | 0% | 100% |
1.25 | 5% | 95% |
1.50 | 10% | 90% |
1.75 | 15% | 85% |
2.00 | 20% | 80% |
See, it's pretty basic calculus. The pivot point is the intangible center point of time of the fully faded in frame.
Writing the Code
So my program takes the following arguments
fade [sequence count] [image1] [image2]
And then emits transition[number].png as the sequence. We are using OpenCV and C. Let me go over the code before I provide the links:
#include <cv.h> #include <cvaux.h> #include <highgui.h> #include <stdio.h>
I create a basic r, g, b structure:
typedef struct { unsigned char r, g, b; } rgb; int main(int argc, char** argv) { int iy = 0, ix = 0, step = 0, phases; rgb *temp1 = 0, *temp2 = 0, *tempOut = 0; char *pStart1, *pStart2, *pOut, name[100]; IplImage *image1 = 0, *image2 = 0, *imageOut = 0;
The number of phases, and the two files that we will be fading:
argv++; phases = atoi(*argv++); image1 = cvLoadImage(*argv++, CV_LOAD_IMAGE_COLOR); image2 = cvLoadImage(*argv++, CV_LOAD_IMAGE_COLOR);
The output frame
imageOut = cvCreateImage(cvGetSize( image1 ), 8, 3);
The number of phases
for(step = 0; step < phases; step++) {
Get the row (that means the horizontal part) pointer for each image so that we can compute the composite pixel.
for(ix = 0; ix < image1->height; ix++) { pStart1 = image1->imageData + ix * image1->widthStep; pStart2 = image2->imageData + ix * image2->widthStep; pOut = imageOut->imageData + ix * imageOut->widthStep; for(iy = 0; iy < image1->width; iy++) {
Create references to the three pixels, one in each image
temp1 = (rgb*)(pStart1 + (iy * 3)); temp2 = (rgb*)(pStart2 + (iy * 3)); tempOut = (rgb*)(pOut + (iy * 3));
Create the Composite Pixel
(validating all this trouble)
We take the channel value for this pixel and multiply it by our step / phase. That's the first part of the channel value. The other part is computed by taking the channel value of the other image's pixel multiplied by (phase - step) / phase which would be the complementary percentage. Repeat this for the other two channels.
tempOut->r = (temp1->r * step) / phases + (temp2->r * (phases - step)) / phases; tempOut->g = (temp1->g * step) / phases + (temp2->g * (phases - step)) / phases; tempOut->b = (temp1->b * step) / phases + (temp2->b * (phases - step)) / phases;
The above code is where you can have a lot of fun. You could do
- Interlaced transitions
- Transitions based on trigonometric functions like cosine
- Random transitions
- Transitions by channel
etc... You have much more control here then you would with some pre-packaged program.
And after all, proposterous levels of customization is the biggest benefit of rolling your own solution.
After all the fun is over, we write the image, and its phases * 2 - step - 1 counterpart
} } sprintf(name, "transition%03d.png", step); cvSaveImage(name, imageOut); sprintf(name, "transition%03d.png", phases * 2 - step - 1); cvSaveImage(name, imageOut); }
And then we clean up:
cvReleaseImage(&image1); cvReleaseImage(&image2); cvReleaseImage(&imageOut); return 0; }
The whole code is here: http://9ol.es/fade.c.txt and the makefile is here: http://9ol.es/Makefile.fade.txt
Creating the Animation
After I compile it, I run
./fade 10 fb.png tw1.png
If I do an ls, I'll see these new files:
$ ls trans*png transition000.png transition001.png transition002.png transition003.png transition004.png transition005.png transition006.png transition007.png transition008.png transition009.png transition010.png transition011.png transition012.png transition013.png transition014.png transition015.png transition016.png transition017.png transition018.png transition019.png
Now we need to combine these images and emit our GIF. The obvious tool that comes to mind is FFmpeg.
FFmpeg
To create an animation that is a composite of these images, I could just use FFmpeg like so:
ffmpeg -i transition%03d.png -r 5 -o out.gif
But a few problems:
- FFmpeg doesn't know how to deal with 8 bit color that well
FFmpeg support for GIF kinda sucks. It doesn't support the "Netscape looping extension" or different forms of frame replacement.
So this means you will get a small, but rather ugly image. On a reddit thread, I used FFmpeg to generate this 22MB 298x168 9min 2059 frame sequence.
It does the job, it's rather small for 2000 frames ... but the horrible dithering is also pretty classically bad.
Palettes
FFmpeg uses the web safe GIF palette, which is 216 colors. There are 6 shades for red, 6 for green, and you guessed it, 6 for blue. The palette is reproduced below via javascript:
Since some colors, especially close ones don't map well to the palette above, I need software that is capable of finding optimal palettes and having generally better control over palettes. When you do this, then you can choose your 256 color palette from a full 24 bit one, with 8 bits for each channel i nthe RGB color space.
Gifsicle
With FFmpeg out of the picture, I decided to use gifsicle. It's great at taking GIFs and making an animation out of them. Only GIFs though, not PNGs. :-(
So, using tcsh and ImageMagick's convert tool again, I converted the PNGs to GIFs:
foreach n(transi*.png) echo "Converting $n" convert $n $n:r.gif end
You can do this type of iteration in bash of course, but bash's syntax when dealing with extension swapping is more cumbersome, in my opinion.
Running this emits:
% foreach n(transi*.png) foreach? echo "Converting $n" foreach? convert $n $n:r.gif foreach? end Converting transition000.png Converting transition001.png Converting transition002.png Converting transition003.png Converting transition004.png Converting transition005.png Converting transition006.png Converting transition007.png Converting transition008.png Converting transition009.png Converting transition010.png Converting transition011.png Converting transition012.png Converting transition013.png Converting transition014.png Converting transition015.png Converting transition016.png Converting transition017.png Converting transition018.png Converting transition019.png
Our file listing now looks like this:
% ls -1 trans* transition000.gif transition000.png transition001.gif transition001.png transition002.gif transition002.png transition003.gif transition003.png transition004.gif transition004.png transition005.gif transition005.png transition006.gif transition006.png transition007.gif transition007.png transition008.gif transition008.png transition009.gif transition009.png transition010.gif transition010.png transition011.gif transition011.png transition012.gif transition012.png transition013.gif transition013.png transition014.gif transition014.png transition015.gif transition015.png transition016.gif transition016.png transition017.gif transition017.png transition018.gif transition018.png transition019.gif transition019.png
We are almost ready to go. I have my PNGs as fully-dithered custom-paletted GIFs. Now I need to use gifsicle and create an animation.
About Style
(lack thereof?)
Gifsicle is so custom palette friendly, that it will create a custom palette for each frame if you let it go unchecked. This can add some file size overhead and will make the animation look fantastic. [60.6K]
Unfortunately, the output isn't very GIF-like any more. Part of GIF's serendipity is its charmingly bad color matching. But only if it's subtle and characteristic. Not when it's atrocious and bludgeoning.
Well, we're in luck because the --colors 64 option satisfies these requirements! Gifsicle will find an optimized palette and then just do nearest color matching (no dithering). [30.5K]

$ gifsicle -O9 --colors 64 -d 20 -l1000 transi*.gif > style.gif
Since we are using blues here, we shouldn't go much lower lest we see ugly stuff. [15.9K]

$ gifsicle -O9 --colors 16 -d 20 -l1000 transi*.gif > uglystuff.gif
If all three of these images look almost identical to you, then it's ok. <gloat>Not everyone is in tune with the finer nuances of skillfull craftsmanship</gloat>.
The above shell script can be found here: http://9ol.es/batch.sh.txt