IdeaMonk

thoughts, ideas, code and other things...

Saturday, July 17, 2010

Poorly implementing caching in python - eye opener

So after looking at some slides on caching function returns in javascript, I was keen on trying out so in Python. And LOL I came up with this logic -
fun(val):
if val in cache.keys():
return cache[val]
else:
do the right thing...

But it seems, though "if val in cache.keys()" sounds very human friendly, it definitely would suck for a very big cache, and so it does in the following test.

I guess I'm not using timeit in the classical way where I would pass some statements in string and ask it to do them for N number of times, but it seems passing some variables from existing code to statement string is a pain, tried global etc, didn't work. Hence a simple time diff test.
# -*- coding: utf-8 -*-
import random
from timeit import Timer

class PlainColorParser:
def parse(self,value):
rgb = int(value[1:], 16)
r = rgb >> 16 & 0xff
g = rgb >> 8 & 0xff
b = rgb & 0xff
return (r,g,b)

def __call__(self,value):
return self.parse(value)

class NoBitColorParser:
def __call__(self,value):
return (int(value[1:3],16), int(value[3:5],16), int(value[5:7],16))

class PoorCachedColorParser(PlainColorParser):
def __init__(self):
self.cache = {}

def __call__(self,value):
if value in self.cache.keys():
return self.cache[value]
self.cache[value] = self.parse(value)
return self.cache[value]

class CachedExceptionColorParser(PlainColorParser):
def __init__(self):
self.cache = {}

def __call__(self,value):
try:
return self.cache[value]
except KeyError:
self.cache[value] = self.parse(value)
return self.cache[value]

class CachedColorParser(PlainColorParser):
def __init__(self):
self.cache = {}

def __call__(self,value):
if value in self.cache:
return self.cache[value]
else:
self.cache[value] = self.parse(value)
return self.cache[value]

if __name__ == "__main__":
t = Timer()
pccParse = PoorCachedColorParser()
cecParse = CachedExceptionColorParser()
ccParse = CachedColorParser()
pcParse = PlainColorParser()
nbParse = NoBitColorParser()

# setup some random data to test
colors = []
for i in xrange(100000):
colors.append("#" + hex(random.randint(0xfe0000, 0xff0aff))[2:])


def timeDiff(obj):
start = t.timer()
for c in colors:
obj(c)
stop = t.timer()
return ((1000000*stop - 1000000*start)/1000000)

# ---- test poorly cached
print "Poorly Cached - %.2fs" % timeDiff(pccParse)

# ---- test exception cached
print "Exception Cached - %.2fs" % timeDiff(cecParse)

# ---- test cached
print "Cached - %.2fs" % timeDiff(ccParse)

# ---- test uncached
print "Non Cached - %.2fs" % timeDiff(pcParse)

# ---- test no bitwise, uncached
print "Not Bitwise, Non Cached - %.2fs" % timeDiff(nbParse)

So we've got 4 5 classes to represent different ways of parsing an html hex code for color e.g. "#f00f00" into a tuple of (r,g,b) integers. PlainColorParser and NoBitColorParser could easily be functions with no need of classes over them as they do not cache, but to bring them a little equal to other two cached ones, I've bound them in classes.

NoBitColorParser does string manipulations and parses 3 times before returning a tuple. PlainColorParser does better than that, it uses bit shifts and AND masks to filter out content after 1 round of parsing integer from string. PoorCachedColorParser does caching in an obvious way "if its there in cache keys... else ...", and CachedColorParser CachedExceptionColorParser complies to the philosophy of "Fail early, fail often", which is quite interesting :D
but recent findings reveal that CachedColorParser is the right, fast, pythonic way.


What the test does is - generate 100000 random color codes, pick them from a range of 2816 colors (0xff0aff - 0xff0000 + 1). Obviously many colors are bound to get repeated says pigeon hole.

Here's what goes on in an average run on my machine -
abhishekmishra@mbp [~/code]> python pycaching.py
Poorly Cached - 340.23s
Exception Cached - 0.30s
Cached - 0.18s
Non Cached - 0.15s
Not Bitwise, Non Cached - 0.16s

"if value in self.cache.keys():" in PoorCachedColorParser gives you a thumbs down with a sucky performance, obviously not the right thing to do!!! (I was mistaken)

CachedExceptionColorParser gives a sweet 0.30s, "Fail early, Fail often" works :)
But wait, CachedColorParser goes further even a bit more with just 0.18s.


NoBitColorParser is suckier than the Non Cached PlainColorParser, which points out that string ops, parsing integers is one costly affair.

So much for food to my sleeplessness. Oh I remember doing something similar in PyMos too :D.

More updates -

Looking at Dhananjay's code on BangPypers, I think I was too excited to throw in the idion of fail fast in this place, it can albeit be done in a cleaner way. So instead of -
try:
return self.cache[value]
except KeyError:
self.cache[value] = self.parse(value)
return self.cache[value]

You could just write in much cleaner way -
if value in self.cache:
return self.cache[value]
else:
self.cache[value] = self.parse(value)
return self.cache[value]


So the issue was with cache.keys(), which now seems like an obvious slow and shitty way.
New stats reveal that even Try: Except:... fail fast is even not the right way.

Notice the almost double time difference between "try.. except.." way and "if value in self.cache".

Lesson learnt :)

Labels: , , ,

Tuesday, March 16, 2010

Fun with PyGame's camera module

OMG T2 starts tomorrow, I'm probably gonna fail in Random Processes this time.
But before I start preparing, I had to try out the tempting Camera Module of PyGame.
PyGame has pretty cool stuff under pygame.transform and pygame.mask, which makes the task of thresholding very easy.
I came up with an interactive Tux based on the "Capturing a Live Stream" code in the Camera Module Introduction.


My program tries to detect red colored objects, finds the centroid of such points and draws a ghost (find in your /usr/share/icons/oxygen/32x32/apps/) and makes Tux (/usr/share/icons/oxygen/128x128/apps/tux.png) run away from it.

Here's how the code looks like -

# -*- coding: utf-8 -*-

# An interactive Tux, interact with it using any red colored object
#
# based on PyGame camera module intro by Nirav Patel
# http://www.pygame.org/docs/tut/camera/CameraIntro.html
# -- Abhishek Mishra (ideamonk # gmail.com)

import os
import pygame
import pygame.camera
from pygame.locals import *

class Capture(object):
''' A Capture class to get location of a desired blob '''

def __init__(self, ccolor=(248, 111, 115), threshold=(60, 10, 10)):
self.size = (640,480)
# create a display surface. standard pygame stuff
self.display = pygame.display.set_mode(self.size, 0)
# initialize camera module
pygame.camera.init()
# this is the same as what we saw before
self.clist = pygame.camera.list_cameras()
if not self.clist:
raise ValueError("Sorry, no cameras detected.")
self.cam = pygame.camera.Camera(self.clist[0], self.size)
self.cam.start()

# create a surface to capture to. for performance purposes
# bit depth is the same as that of the display surface.
self.snapshot = pygame.surface.Surface(self.size, 0, self.display)
# target color to detect -- default is red
self.ccolor = ccolor
# by default we give more priority to shades of red
self.threshold = threshold

def get_blob_location(self):
self.snapshot = self.cam.get_image(self.snapshot)
# threshold against the color we got before
mask = pygame.mask.from_threshold(self.snapshot, self.ccolor, self.threshold)
# keep only the largest blob of that color
connected = mask.connected_component()
# these numbers are purely experimental and specific to your room and object
# print mask.count() # use this to estimate
# make sure the blob is big enough that it isn't just noise
if mask.count() > 7:
# find the center of the blob
return mask.centroid()
return (None,None)

class Ghost():
''' Ghost class, to have a rect for collision detection '''
def __init__(self):
self.image = pygame.image.load (os.path.join ("./","gv.png"))
self.rect = self.image.get_rect()

def set_rect(self,position):
self.left,self.top=position
self.rect = pygame.Rect(self.left,self.top,32,32)

class Tux(Capture):
''' Tux class extends Capture and does stuff using get_blob_location '''

def __init__(self):
Capture.__init__(self)
self.location = [ x/2 for x in self.size ]
self.set_rect (self.location)
self.image = pygame.image.load (os.path.join ("./","tux.png"))
self.ghost = Ghost()
self.backbuffer = pygame.Surface (self.size)
self.force = 5

def set_rect(self,position):
left,top=position
self.rect = pygame.Rect(left,top,128,128)

def interact_tux(self):
if (pygame.sprite.collide_rect(self,self.ghost)):
# ghost collides with tux
if self.ghost.left<self.location[0]+64:
self.location[0] += self.force
if self.ghost.left>self.location[0]+64:
self.location[0] -= self.force
if self.ghost.top<self.location[1]+64:
self.location[1] += self.force
if self.ghost.top>self.location[1]+64:
self.location[1] -= self.force


def main(self):
going = True
old_coord = (0,0)

while going:
events = pygame.event.get()
for e in events:
if e.type == QUIT or (e.type == KEYDOWN and e.key == K_ESCAPE):
# close the camera safely
self.cam.stop()
going = False

new_coord = self.get_blob_location()
if new_coord != (None,None):
# delta = sum( [(x-y)**2 for (x,y) in zip(new_coord,old_coord)]) # for less fuzziness
# if delta>200:
old_coord = new_coord
self.ghost.set_rect(old_coord)

self.set_rect (self.location)
self.interact_tux()
self.backbuffer.blit(self.snapshot,(0,0))
self.backbuffer.blit(self.image,self.location)
self.backbuffer.blit(self.ghost.image, old_coord)
self.backbuffer = pygame.transform.flip(self.backbuffer,True,False)
self.display.blit(self.backbuffer,(0,0))
pygame.display.flip()

if __name__=='__main__':
t = Tux() # all default params
t.main()
And that's me doing weird things with tux :D -




Kalman Filter
would be more interesting.

Labels: , , ,