Pixel Access Objects

I just added support for a new “pixel access” method to PIL 1.1.6. The load method now returns a pixel access object, which behaves like a 2-dimensional mapping. You can use the access object on both sides of an assignment statement; as an expression, it fetches the given pixel value, and as an assignment target, it updates the image:

pix = im.load()

# get pixel value
print pix[x, y]

# put pixel value
pix[x, y] = value

Performance is pretty good. For example, here are reimplementations of the standard point method. The first one uses the “old” pixel API (getpixelputpixel), while the latter uses the new array API:

def old_point(im, lut):
    out = Image.new(im.mode, im.size, None)
    for y in xrange(im.size[0]):
        for x in xrange(im.size[1]):
            out.putpixel((x, y), lut[im.getpixel((x, y))])
    return out

def new_point(im, lut):
    out = Image.new(im.mode, im.size, None)
    ipixel = im.load()
    opixel = out.load()
    for y in xrange(im.size[0]):
        for x in xrange(im.size[1]):
            opixel[x, y] = lut[ipixel[x, y]]
    return out

On a 2.8 GHz PC, using a 512×512 grayscale image, the old_point implementation needs 1.9 seconds to process the image (~140,000 pixels per second), while the new_point version needs 0.15 seconds (~1,750,000 pixels per second). Not too bad.

On the other hand, for a trivial example like this, you can use the getdata and putdata bulk operations instead:

def bulk_point(im, lut):
    out = Image.new(im.mode, im.size, None)
    out.putdata([lut[x] for x in im.getdata()])
    return out

On this machine, this is twice as fast as the new_point version. And point is of course even faster; it needs 0.7 milliseconds for this test.

But being able to process nearly 2 million pixels per second in Python on average hardware is pretty amazing. When I started working with image processing, you had to use hand-tuned assembler to get anywhere near to that kind of performance.