Skip to main content

FastPixelPerfect — wiki

This function checks if two objects truly collide. This is useful when you have two objects whose rects collide even when they are not actually touching each other, and you want to know when the actual objects collide. This code will check 1000 collisions between 2 rects that are 25 * 10 in size in about 0.015 to 0.07 seconds. PyGame's rect.colliderect will do the same collisions in 0.001 seconds.

All objects must have a 'rect' attribute and a 'hitmask' attribute. The 'blank' attribute is voluntary, as 0 will most likely be its value, and using the pixel as a boolean statement would then work to the same effect. The hitmask can be any type of surfarray, but you will most likely use array_alpha(image) or array_colorkey(image).

The following is a revision of Joshua Gram's version, by Alfonso Crawford.

def PixelPerfectCollision(obj1, obj2):
    """
    If the function finds a collision, it will return True;
    if not, it will return False. If one of the objects is
    not the intended type, the function instead returns None.
    """
    try:
        #create attributes
        rect1, mask1, blank1 = obj1.rect, obj1.hitmask, obj1.blank
        rect2, mask2, blank2 = obj2.rect, obj2.hitmask, obj2.blank
        #initial examination
        if rect1.colliderect(rect2) is False:
            return False
    except AttributeError:
        return None

    #get the overlapping area
    clip = rect1.clip(rect2)

    #find where clip's top-left point is in both rectangles
    x1 = clip.left - rect1.left
    y1 = clip.top  - rect1.top
    x2 = clip.left - rect2.left
    y2 = clip.top  - rect2.top

    #cycle through clip's area of the hitmasks
    for x in range(clip.width):
        for y in range(clip.height):
            #returns True if neither pixel is blank
            if mask1[x1+x][y1+y] is not blank1 and \
               mask2[x2+x][y2+y] is not blank2:
                return True

    #if there was neither collision nor error
    return False

Note: You can see an example using an algorithm almost identical to Joshua's at John Eriksson's project, PixelPerfect.

Here is a cleaned up and bug-fixed version of the original by RB[0],
that now uses the for loop(as suggested here by Joshua Grams), instead of while loops, which are slower.
There are also a few helper functions that are used to create hitmasks without using pygames surfarray module, as it is currently broken(at least for me), these dont even require Numeric.

This pixelperfect implementation is anywhere from 1.2 to 2.5 times faster than the one shown above.

def check_collision(obj1,obj2):
    """checks if two objects have collided, using hitmasks"""
    try:rect1, rect2, hm1, hm2 = obj1.rect, obj2.rect, obj1.hitmask, obj2.hitmask
    except AttributeError:return False
    rect=rect1.clip(rect2)
    if rect.width==0 or rect.height==0:
        return False
    x1,y1,x2,y2 = rect.x-rect1.x,rect.y-rect1.y,rect.x-rect2.x,rect.y-rect2.y
    for x in xrange(rect.width):
        for y in xrange(rect.height):
            if hm1[x1+x][y1+y] and hm2[x2+x][y2+y]:return True
            else:continue
    return False

def get_colorkey_hitmask(image, rect, key=None):
    """returns a hitmask using an image's colorkey.
       image->pygame Surface,
       rect->pygame Rect that fits image,
       key->an over-ride color, if not None will be used instead of the image's colorkey"""
    if key==None:colorkey=image.get_colorkey()
    else:colorkey=key
    mask=[]
    for x in range(rect.width):
        mask.append([])
        for y in range(rect.height):
            mask[x].append(not image.get_at((x,y)) == colorkey)
    return mask

def get_alpha_hitmask(image, rect, alpha=0):
    """returns a hitmask using an image's alpha.
       image->pygame Surface,
       rect->pygame Rect that fits image,
       alpha->the alpha amount that is invisible in collisions"""
    mask=[]
    for x in range(rect.width):
        mask.append([])
        for y in range(rect.height):
            mask[x].append(not image.get_at((x,y))[3]==alpha)
    return mask

def get_colorkey_and_alpha_hitmask(image, rect, key=None, alpha=0):
    """returns a hitmask using an image's colorkey and alpha."""
    mask=[]
    for x in range(rect.width):
        mask.append([])
        for y in range(rect.height):
            mask[x].append(not (image.get_at((x,y))[3]==alpha or\
                                image.get_at((x,y))==colorkey))
    return mask

def get_full_hitmask(image, rect):
    """returns a completely full hitmask that fits the image,
       without referencing the images colorkey or alpha."""
    mask=[]
    for x in range(rect.width):
        mask.append([])
        for y in range(rect.height):
            mask[x].append(True)
    return mask
For an example of use:
store the above code as pixelperfect.py,
download this image http://www.mediafire.com/?f4yz4mm2o1m and save it as carrots.png, in the same directory
Now, create a new file(in the same folder) with the following code:
import pygame, pixelperfect, time
from pygame.locals import *
from pixelperfect import *

def load_image(name, colorkey=None, alpha=False):
    """loads an image into memory"""
    try:
        image = pygame.image.load(name)
    except pygame.error, message:
        print 'Cannot load image:', name
        raise SystemExit, message
    if alpha:image = image.convert_alpha()
    else:image=image.convert()
    if colorkey is not None:
        if colorkey is -1:
            colorkey = image.get_at((0,0))
        image.set_colorkey(colorkey, RLEACCEL)
    return image, image.get_rect()

class my_object(object):
    def __init__(self, image,colorkey=None,alpha=None):
        self.image, self.rect=load_image(image,colorkey=colorkey, alpha=alpha)
        if colorkey and alpha:
            self.hitmask=get_colorkey_and_alpha_hitmask(self.image, self.rect,
                                                        colorkey, alpha)
        elif colorkey:
            self.hitmask=get_colorkey_hitmask(self.image, self.rect,
                                              colorkey)
        elif alpha:
            self.hitmask=get_alpha_hitmask(self.image, self.rect,
                                           alpha)
        else:
            self.hitmask=get_full_hitmask(self.image, self.rect)

pygame.init()
screen = pygame.display.set_mode([200,200])
screen.fill([255,255,255])


a=my_object('carrots.png',-1,None)
a.rect.center=(25,25)

b=my_object('carrots.png',None,True)
b.rect.center=(50,50)

screen.blit(a.image, a.rect)
screen.blit(b.image, b.rect)
pygame.display.flip()

def main():
    av=0
    for i in xrange(999):
        st_time=time.clock()
        check_collision(a, b)
        av+=time.clock()-st_time

    print "time:", av, check_collision(a, b)

main()

If you have any suggestions, please email me at "roebros (at) gmail.com"