This article discusses making a GIF which fades between three different images of different ideal color palettes, using Numpy to determine the transitions. The code that produced this output is here.
The Python Imaging Library (PIL) in python provides for robust image manipulation including animated GIFs.
My first attempt at making a GIF was very simple. It was quick to find an example of using the GIF functionality of PIL, called images2gif. I spat out a dozen png’s of the data I wanted to animate and turned it into a GIF.
from PIL import Image from images2gif import writeGif images = [Image.open('%s.png' % i) for i in range(12)] durations = ['1.5']*len(images) writeGif('my.gif',images, duration=durations)
With simple images of a few colors, all using the same colors, and no transitions that caused new colors, this process went very quickly. However, to make animated GIFs where the frames of the animation have very different colors, it turns out there are a few unexpected turns.
The GIF uses an indexed color palette. Each pixel of a GIF image is an 8-bit pointer to a color in a color map. That’s 256 possible colors, or, 255 possible colors and one transparency layer. By default, PIL uses a standard web safe palette when converting images with more than this many colors to this format. This will cause reduced quality.
img = PIL.Image.open('some.png') img2 = img.convert("P") #Convert true color image to Palette. img2.save('some.gif') #web safe representation of the png
For better quality, we should use an adaptive palette. I found the web safe palette butchered my png’s, but the adaptive palette result was not distinguishable from the png to me.
img = PIL.Image.open('some.png') img2 = img.convert("P",palette=PIL.Image.ADAPTIVE) #Convert true color image to Palette. img2.save('some.gif') #web safe representation of the png
But if some frames of the animation use different colors, then the ideal palettes for those images will be different. The GIF will only use one palette for the whole animation, and so the colors will distort on those frames of different colors (or this is what it seems to me based on many frames having crazy colors).
The use of PIL.Image.blend to create fading transitions makes this problem even worse, as it creates intermediate colors.
Get the Best Palette with GIMP
For me, the solution was to use GIMP to give all three distinct images the same palette. To do this, have all relevant images open in GIMP as different layers. If you don’t have this already, just open one in gimp and import the others as layers. Goto Image->Mode->Indexed and select an optimized palette of 255 colors. Assuming all layers are stacked on top of each other and only the top frame is visible, just use File->Export as an aptly named png. Then make that layer invisible and export the next layer as a png, and so on.
Transitions with Numpy Masks
Once we have imported the images as index or palette mode png’s, there next step is generating the transition frames. We can use Image.composite which will replace one image by the next pixel by pixel, using masks we can define as numpy arrays. This avoids creating intermediate colors that won’t be in the palette. Slicing numpy arrays makes many transition patterns fairly easy.
def v_fade(width = 8): '''make masks for fading from one image to the next through a vertical sweep. Does this through numpy slices''' global img1 #from img1 = Image.open('one.png') n = np.array(img1) #make a numpy.array the same size as the frame n.dtype=np.uint8 #correct the data type n[:][:] = 0 #the first mask lets everything through #copy the array, else all masks will be the last one result=[Image.fromarray(n.copy())] for i in range(n.shape/width+1): #add vertical strips of width one at a time. y=i*(width) n[y:y+width][:] = 255 result.append(Image.fromarray(n.copy())) return result
Then we simply need a loop that calls Image.composite for each mask, and creates a list of images and durations for the GIF animation.
img1, img2 = (Image.open('%s' % x) for x in ['1.png','2.png']) #fade from 1 to 2 images = [Image.composite(img2,img1,mask) for mask in v_fade()] #fast on transition frams but hold end images longer durations = ['.10']*len(images) durations = durations[-1] = '2.5' #fade from 2 to 1 images.extend([Image.composite(img1,img2,mask) for mask in v_fade()]) durations.extend(durations) writeGif('my.gif',images, duration=durations)
Once you know how to do it, making transitions in animated GIFs with python is very clean looking.