Animating Algorithms with Pygame

When I was working on the Pygame version of Logic Circuits, I was frustrated that I couldn't demonstrate the circuits in action from within the text write-up. I'm somewhat familiar with GIF animations and I saw how they would work for exactly that. Pygame can save an image to the disk when it displays a new frame on the screen and ImageMagick can combine these disk images into a single GIF animation. This worked well for that project and I believe using the animations made the presentation much more understandable.

I have started using this approach in other projects to help the reader (and even the author) gain a better intuition about how many of the classic algorithms work. Our brains evolved to easily grasp and understand things in motion and things undergoing change. Evolution to easily grasp and understand abstract math concepts somehow seemed to have gotten shortchanged.

A quick look at the extra modules used in the Pygame simulations can be done either before or after reading about the algorithms themselves. This list will hopefully grow over the next few months.

The Minimum Spanning Tree Algorithm

The K-means Algorithm

Extra Modules for Pygame Projects

Simple Argument Extraction

The module sarg is used quite a bit. It lets us put key=value pairs on the command line and builds a dictionary for later lookups. It's handy when lots of values may be specified for different modules and requiring hardly any overhead in coding for them. If a pair is left out defaults are provided. Here is the code

# sarg.py - simple argument parser

def _parse() :
   import sys
   global _cmd_dict
   _cmd_dict = {}

   cmds = sys.argv[1:]
   cmdi = 0

   for cmd in cmds :
       cmdi += 1
       parts = cmd.split("=")
       if len(parts) == 1 : _cmd_dict[cmdi] = parts[0]
       else :
           tag,value = parts
           _cmd_dict[tag] = value  # store all vals as strings

def Int(tag,default=0) :
   return int(_cmd_dict.get(tag,default))

def Float(tag,default=0.0) :
   return float(_cmd_dict.get(tag,default))

def Str(tag,default="") :
   return str(_cmd_dict.get(tag,default))

_parse()

A typical command line with arguments might look like

$ python kmeans.py pixels=300 prefix=kmeans maxpics=40

and later calls to extract the values might look like

pixels = sarg.Int("pixels", 600)
prefix = sarg.Str("prefix")

When sarg is imported for the first time the dictionary _cmd_dict (private by convention) is assembled from the command line sys.argv[1:]. Key/value pairs can be used anywhere in the program and converted from string to integer or float. A default value may be provided (as with pixels above) in case the key value pair is missing on the command line. Or if missing on the command line and no default is provided in the extraction then an empty string or zero is returned. In these Pygame samples some keys are common, operating the graphics and generation of static images and gifs. To add a new key just use it somehwere in the program to extract a value and occasionally add the pair to the command line.

Pycon and the camera

The pycon module provides a somewhat simpler interface to pygame for the graphic projects using it. It also provides a simple mechanism to generate little GIF movies. Pycon also using the sarg module to extract control values.

class Pgcon :
   def __init__ (self) :
       import pygame as pg
       pg.init()
       self.pg   = pg
       self.size   = sarg.Int("pixels",600)
       self.frame  = 0
       self.camera = camera.Camera (pg)
       self.screen = pg.display.set_mode((self.size,self.size))

   def newScreen (self, background=BGCOLOR) :
       self.screen.fill(background)

   def lite(self, color) :
       # for example white (255,255,255) becomes gray (127,127,127)
       return [x/2 for x in color]

   def textDraw(self, color, pos, text) :
       font = self.pg.font.Font(None,24)
       rend = font.render(text,True,color)
       tw,th = font.size(text)
       org = (pos[0]-tw/2, pos[1]-th/2)
       self.screen.blit(rend, org)

   def lineDraw(self,color,apos,bpos,width=1) :
       self.pg.draw.line(self.screen, color, apos, bpos, width)

   def writeScreen (self, wait=.1) :
       self.pg.display.flip()
       time.sleep(wait)
       self.camera.takePicture(self.screen)

   def close (self, pause=True) :
       if pause : raw_input ("Hit Return to exit")
       self.camera.makeGif()
       self.pg.quit()

Only a single instance of the Pycon class is useful.

The code should be very straight-forward. Asking for a newscreen just fills the display with the background color. The geometry is square and defaults to 600 pixels. But in the example graphics in the project write-ups pixels=300 is generally used.

The attribute pg is simply a reference to pygame as imported. It is passed around as needed.

The method textDraw draws text in a standard 24pt font and centered on the position passed. This make it easy to plop labels over a point on the plot or to center the banner on the top of the screen.

The method writeScreen does a pygame display flip and a wait.

Finally, pgcon.close() normally waits for the user to release the final frame from the screen. It then instructs the camera to possibly create a gif animation from any frames that it has saved to disk as .png files.

The Camera

A Camera instance is always attached to a pycon. Pygame has a nifty function image.save to convert an image to a standard format (like jpeg or png) and write it to the disk. This file can then by opened by a program like the linux eog to display it on the screen. The file can also be loaded and viewed in a browser.

The camera typically captures a set of images that can be played back either as a slide show or little movie. The latter is done from the ImageMagick program convert. You must install ImageMagick on your computer to use this.

When the display is about to be updated, the camera may be armed to save the image to disk. At the end of the run the images saved may be converted into a gif animation. Both of these steps are optional.

The camera has a number of attributes to control what it does. These are set through sarg explained above in the current implementation.

  • maxpics: the maximum number of images to capture. Mainly a safety feature to prevent a runaway program from doing to much damage (required to save images)
  • prefix: a name prefix for both the png files and the gif animation. (required to save images)
  • gif: set to 1 if you want the camera to create a gif
  • keep_pngs: if a gif is made, setting this to 1 keeps the pngs from being removed.
  • skip: set to number of frames to ignore at the beginning. Rarely used.

The following is a simple example from the kmeans project. It captures 12 frames, converts the png files to a single gif and then deletes the png files. Adding keep_pngs=1 to the command lines would leave both pngs and the gif.

$ python kmeans.py pixels=300 prefix=km maxpics=40 gif=1 seed=180
Just captured frame: 1, km_001.png
Just captured frame: 2, km_002.png
Just captured frame: 3, km_003.png
Just captured frame: 4, km_004.png
Just captured frame: 5, km_005.png
Just captured frame: 6, km_006.png
Just captured frame: 7, km_007.png
Just captured frame: 8, km_008.png
Just captured frame: 9, km_009.png
Just captured frame: 10, km_010.png
Just captured frame: 11, km_011.png
Just captured frame: 12, km_012.png
Hit Return to exit
Making uniform GIF
km.gif has 12 frames
class Camera :
   def __init__ (self, pg) :
       self.maxpics   = sarg.Int("maxpics",0)
       self.prefix    = sarg.Str("prefix")
       self.gif       = sarg.Int("gif",0)
       self.skip      = sarg.Int("skip",0)
       self.picsTaken= 0
       self.armed = False
       self.pg    = pg

   def takePicture(self, screen) :
       if self.picsTaken>=self.maxpics or not self.armed or not self.prefix :
           return False
       self.picsTaken += 1
       file = "%s_%03d.png" % (self.prefix, self.picsTaken)
       if self.skip <= self.picsTaken :
           self.pg.image.save(screen, file)
           print "Just captured frame: %d, %s"%(self.picsTaken, file)
       self.armed = False
       return True

   def makeGif(self, loop=1, delay=50) :
       import os
       if not self.picsTaken or not self.gif : return
       prefix = self.prefix
       if type(delay) == type(99) :
           print "Making uniform GIF"
           cmd = "convert -delay %d -loop %d %s_*.png %s.gif"
           cmd = cmd % (delay, loop, prefix,prefix)
       else :
           print "Making variable spaced gif"
           dptr = 0
           cmd = ['convert -loop %d' % loop]
           for i in range(self.picsTaken) :
               if i < self.skip : continue
               delta = delay[i%len(delay)]
               cmd.append(" -delay %s %s_%03d.png" %(delta,prefix,i+1))
           cmd.append(" %s.gif" % prefix)
           cmd = " \\\n  ".join(cmd)
       # create the .gif file with ImageMagick
       os.system(cmd)
       if not sarg.Int("keep_pngs", 0) :
           os.system("rm %s_*.png" % prefix)
       print "%s.gif has %d frames" % (prefix,self.picsTaken)