views:

242

answers:

4

I need to replace all the white(ish) pixels in a PNG image with alpha transparency.

I'm using Python in AppEngine and so do not have access to libraries like PIL, imagemagick etc. AppEngine does have an image library, but is pitched mainly at image resizing.

I found the excellent little pyPNG module and managed to knock up a little function that does what I need:

make_transparent.py

pseudo-code for the main loop would be something like:

for each pixel:
    if pixel looks "quite white":
        set pixel values to transparent
    otherwise:
        keep existing pixel values

and (assuming 8bit values) "quite white" would be:

where each r,g,b value is greater than "240" 
AND each r,g,b value is within "20" of each other

This is the first time I've worked with raw pixel data in this way, and although works, it also performs extremely poorly. It seems like there must be a more efficient way of processing the data without iterating over each pixel in this manner? (Matrices?)

I was hoping someone with more experience in dealing with these things might be able to point out some of my more obvious mistakes/improvements in my algorithm.

Thanks!

A: 

I'm quite sure there is no short cut for this. You have to visit every single pixel.

InsertNickHere
+1  A: 

Honestly, the only heuristic I could conceive is picking a few arbitrary, random points on your image and using a flood fill.

This only works well if your image as large contiguous white portions (if your image is an object with no or little holes in front of a background, then you're in luck -- you actually have a heuristic for which points to flood fill from).

(disclaimer: I am no image guru =/ )

Justin L.
I had a go at implementing this last night, it was fun exercise, but ultimately either required a significantly larger amount of memory (to store the current known boundaries) or I had to visit pixels more than once each.... interesting though, so thanks.
ozone
How were you implementing your flood fill? You shouldn't have to revisit pixels or store boundaries, if you're using the recursive implementation.
Justin L.
+1  A: 

This still visits every pixel, but may be faster:

new_pixels = []
for row in pixels:
    new_row = array('B', row)
    i = 0
    while i < len(new_row):
        r = new_row[i]
        g = new_row[i + 1]
        b = new_row[i + 2]
        if r>threshold and g>threshold and b>threshold:
            m = int((r+g+b)/3)
            if nearly_eq(r,m,tolerance) and nearly_eq(g,m,tolerance) and nearly_eq(b,m,tolerance):
                new_row[i + 3] = 0
        i += 4
    new_pixels.append(new_row)

It avoids the slicen generator, which will be copying the entire row of pixels for every pixel (less one pixel each time).

It also pre-allocates the output row by directly copying the input row, and then only writes the alpha value of pixels which have changed.

Even faster would be to not allocate a new set of pixels at all, and just write directly over the pixels in the source image (assuming you don't need the source image for anything else).

Saxon Druce
Also by the way, your original code is checking nearly_eq(r,...) three times, instead of r, g and b.
Saxon Druce
I got roughly a 25% speed increase from this thanks! ....I tried directly manipulating the original pixel array, but that required loading the whole image into memory, which ended up causing an overall slowdown.
ozone
A: 

The issue seems to have more to do with loops in Python than with images.

Python loops are extremely slow, it is best to avoid them and use built-ins loop operators instead.

Here, if you were willing to copy the image, you could use a list comprehension:

def make_transparent(pixel):
  if pixel looks "quite white": return transparent
  else: return pixel

newImage = [make_transparent(p) for p in oldImage]
Matthieu M.