Inigo Quilez   ::     ::  
I remember years ago to be toying around with genetic algorithms. After I found a couple of blogs out there that posted on the topic few months ago, I decided to do some experiments again and see how much genetic algorithms could assist in extreme image compression.

The experiment

The idea was to give myself a budget of 1024 colored points/disks and see how they could be best arranged to make an image. I explain: each point had a position, size and color (alpha channel/transparency included). Provided the image is not bigger than 256x256 pixels, then all these 1024 points can be represented in no more than 6 kilobytes of data. Thankfully, as soon as there are some bias from a pure random distribution a good compressor will produce a smaller output than those 6 kilobytes. Anyway, the trick was to chose the right point position, size and colors for every point. And that's of course where the genetic algorithm can help us: given a reference image we let the algorithm find those positions and colors for us.

I'm not gonna explain here what genetic algorithms are, there is a good amount of resources out there that I don't want to duplicate. What probably matters is that my implementation was quite naive, but even so, just after few hours I got my compressed image (see photo in the right side of this text). Not bad. So, the experiment shows that indeed one can use genetic algorithms to help in image compression. This experiment proved that even a simple implementation can produce images that are perhaps good for 64 kilobyte demos. I'm sure that with a bit of work in the mutation strategy and the data layout one can get some nice images with only 4 kilobytes.

Image encoded in 5 kilobytes

The sketch of the implementation

The thing was coded in C, but I write it in pseudocode here. data contains the position, size and color of the points, and its layout is optimized for rendering, not for compression. Rendering and image comparison are the slowest steps in the algorithm, so having the data optimized for rendering speed is a good idea. The data is rearranged and optimized for compression only after the best data has been found, and it's done only once. Care should be taken, tho, so the speed optimized and size compressed data both render the same images, otherwise the algorithm could be converging to the wrong fixed point.

// init image = loadImage( "image.png" ); data = randomdata(); difference = infinite; // compute while( !happy ) { newdata = randomize( data ); newimage = renderImage( newdata ); newdifference = computeDifferenceMSE( newimage, image ); if( newdifference < difference ) { data = newdata; difference = newdifference; // data contains the compressed/encoded image saveToDisk( data ); } }

The renderImage() function was implemented in OpenGL. However, computeDifference was done in C, but could have been optimized by implementing it in OpenGL too by image differencing and few reduction passes (log(imageResolution)/log(2) passes, to be more precise). For my experiment I use the traditional euclidean distance between the two images, ie, MSE.

Some considerations

Well, the code seems simple, but the real trick here is to find a nice randomize() function. Usually, such a function randomizes one or a few of the values of data by a small amount, in an attempt to do a random walk towards the minima of the function. Of course, as it often happens with these methods, there is no one but many minimas, which are local minimas but most of the times not the best minima possible. So, from time to time it is a good idea to generate a completely random sequence from scratch, and let the mutation start a new evolutionary line. Well, there is a long theory you could read out there, and learn on mutation strategies. You will end up reading about concepts like ergodicity, methods like metropolis, and stuff. Certainly too much stuff to be described here. The good news is that you don't really need all that to get started and play around a bit - common sense is probably more than enough.


This is an example of the algorithm can produce, by using only 1000 points: