I have a Python script that creates a diff of two images using PIL. That part works fine. Now I need to find an efficient way to count all the non-black pixels (which represent parts of the two images that are different). The diff image is in RGB mode.

My initial cut was something like this:

return sum(x != (0, 0, 0) for x in diffimage.getdata())

Then I realized that the diffs were usually constrained to a portion of the image, so I used getbbox() to find the actual diff data:

bbox = diffimage.getbbox()
return sum(x != (0, 0, 0) for x in diffimage.crop(bbox).getdata()) if bbox else 0

This has the advantage of being VERY fast when the image is all black since bbox is None in that case and no pixel counting need be done.

I still wasn't satisfied, so I decided to try using more of the built-in PIL methods to avoid the generator expression with the Python conditional that needed to be evaluated for each pixel. I came up with:

bbox = diffimage.getbbox()
if not bbox: return 0
return sum(diffimage.crop(bbox)
                    .point(lambda x: 255 if x else 0)

This is about five times faster than the previous version. The basic steps are:

  1. Crop to the bounding box to avoid counting black pixels
  2. Convert all non-zero values in each channel to 255. This way, when we later convert it to grayscale, all non-black pixels are guaranteed to have non-zero values. (Because a pixel might differ in only one channel, and only by a small amount, some pixels that are not actually black might end up as black in grayscale mode, because only a fraction of that channel's value makes its way to grayscale.) BTW, the function isn't evaluated for each pixel but only once for each possible pixel value to make a lookup table, so it's very fast.
  3. Convert to grayscale.
  4. Convert all non-zero pixels to 1 using bool.
  5. Sum all the pixel values.

Can I do better?

up vote 5 down vote accepted

Here's your implementation using Pillow:

def count_nonblack_pil(img):
    """Return the number of pixels in img that are not black.
    img must be a PIL.Image object in mode RGB.

    bbox = img.getbbox()
    if not bbox: return 0
    return sum(img.crop(bbox)
               .point(lambda x: 255 if x else 0)

And here's an implementation using Numpy:

def count_nonblack_np(img):
    """Return the number of pixels in img that are not black.
    img must be a Numpy array with colour values along the last axis.

    return img.any(axis=-1).sum()

(We will need scipy.ndimage.imread to load the image.)

Here's a quick performance comparison using timeit.timeit:

>>> from PIL import Image
>>> import scipy.ndimage
>>> from timeit import timeit
>>> img1 = Image.open(filename)
>>> timeit(lambda:count_nonblack_pil(img1), number=10)
>>> img2 = scipy.ndimage.imread(filename)
>>> timeit(lambda:count_nonblack_np(img2), number=10)

So Numpy is about two and a half times as fast on my example.

  • Nice! It probably won't be any problem using Numpy, either. – kindall Jul 4 '14 at 18:21

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.