Pixel Access Objects
Fredrik Lundh | October 2005 | Originally posted to online.effbot.org
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 (getpixel, putpixel), 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): for x in xrange(im.size): 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): for x in xrange(im.size): opixel[x, y] = lut[ipixel[x, y]] return out
On a 2.8 GHz PC, using a 512x512 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.