You write newArray = imageArray
but this just means that newArray
is another name for the same array. This means that your threshold
function overwrites the original image, which can be very inconvenient (especially when testing). You might want to take a copy of the image:
newArray = imageArray.copy()
In this group of lines:
balanceAr = []
for eachRow in imageArray:
for eachPix in eachRow:
avgNum = reduce(lambda x, y: x + y, eachPix[:3])/float(len(eachPix[:3]))
balanceAr.append(avgNum)
you are computing a (flattened) array balanceAr
whose entries are the mean values of the first three channels for each pixel. You do so by looping over each pixel in the image. But NumPy is most efficient when you can vectorize the code and compute the result for all pixels in one operation. In this case you can use NumPy's fancy indexing to get the first three channels of the image:
colour_channels = imageArray[...,:3]
and then call numpy.mean
to get the average for each pixel:
balanceAr = colour_channels.mean(axis=-1)
(This constructs a 2-dimensional array: if you really wanted a flattened version you could call the flatten
method, but that's not necessary as I will explain below.)
In this line:
balance = reduce(lambda x , y: x + y , eachPix[:3]/float(len(balanceAr)))
It looks as though your intention was to compute the mean value of balanceAr
, but you messed up and only replaced one of the occurrences of eachPix[:3]
by balanceAr
. So obviously this computes the wrong result.
What you need, of course, is:
balance = balanceAr.mean()
In the next group of lines you replace pixels in the image that have a higher mean colour channel than balance
by white, and a lower mean by black. Again, you should vectorize this operation. You can compute a mask array, a Boolean array that is True
for the pixels that are higher than average:
mask = balanceAr > balance
Construct an empty image of the right size:
result = np.empty(imageArray.shape)
Set pixels in the mask to white and other pixels to black:
result[mask] = (255, 255, 255, 255)
result[~mask] = (0, 0, 0, 255)
Thinking about this algorithm more carefully, it's clear that you don't actually need to take the average of the colour channels. The division by 3 is always the same, so it can simply be omitted, and we could use the sum of the colour channels instead. (Calling numpy.sum
instead of numpy.mean
.)
Putting all that together, here's how I'd program it:
import numpy as np
WHITE = np.array((255, 255, 255, 255), dtype=np.uint8)
BLACK = np.array(( 0, 0, 0, 255), dtype=np.uint8)
def threshold2(img, high=WHITE, low=BLACK):
"""Return a new image whose pixels are `high` where pixels in `img`
have a higher sum of colour channels than the average for the
image, and `low` elsewhere.
"""
colsum = img[...,:3].sum(axis=-1)
mask = colsum > colsum.mean()
result = np.empty(img.shape, dtype=np.uint8)
result[mask] = high
result[~mask] = low
return result
This is about 200 times faster than your code:
>>> from timeit import timeit
>>> img = np.random.randint(0, 256, (400, 400, 4))
>>> timeit(lambda:threshold2(img), number=1) # mine
0.05198820028454065
>>> timeit(lambda:threshold(img), number=1) # yours
10.539333346299827
The sum of the colour channels of an image is a bit like the luminance of an image, except that it doesn't take into account the different physiological responses to the channels (green is perceived as brighter than red which is perceived as brighter than blue). Perhaps you should be using 0.2126 R + 0.7152 G + 0.0722 B instead of R + G + B?
If that's right, you need something like this:
# sRGB luminosity coefficients, plus 0 for the alpha channel
LUMINOSITY = np.array((0.2126, 0.7152, 0.0722, 0))
def threshold3(img, high=WHITE, low=BLACK, luminosity=LUMINOSITY):
"""Return a new image whose pixels are `high` where pixels in `img`
have a higher luminance than the average for the image, and `low`
elsewhere. The optional `luminosity` argument provides the
multipliers for the red, green and blue channels.
"""
luminance = (img * luminosity).sum(axis=-1)
mask = luminance > luminance.mean()
result = np.empty(img.shape, dtype=np.uint8)
result[mask] = high
result[~mask] = low
return result
RuntimeWarning: overflow encountered in ubyte_scalars
? – Gareth Rees Feb 28 '14 at 16:34