# Pixelating black and white images

I am doing a function that pixelates black and white images. The function must be called pixelate(img, (pixel_width, pixel_height), mode).

The parameter (pixel_width, pixel_height) is a tuple which indicates the size of the resulting pixel values. the function must return a result in any event, even if they are higher than the image dimensions. If the image size is not a multiple of the pixel size,some of the resulting pixels will have a smaller size than (pixel_width, pixel_height). This smaller pixels will be located in the right and bottom edge of the image as shown in the examples below. In other words, the pixels always start from the top left corner of the image.

The parameter mode, is an string, 'min', 'max' or 'mean'. We will write 'mean', if we want to calculate the mean value of the area where a pixel of the new image is going to be. We will write 'max' if we want the brightest value or 'min' for the darkest. If the mean value has decimals, we will only use the integer part.

Now I have the program done, but I want to do it writing the shortest possible code, to use as little CPU memory and time as possible.

from PIL import Image

import numpy as np

def pixelate_mode(mode):
if mode == 'mean':
return np.mean
elif mode == 'min':
return min
elif mode == 'max':
return max

def pixelate(img, (pixel_width, pixel_height), mode):
width, height = img.size
W = 0
while W * pixel_width < width:
H = 0
while H * pixel_height < height:
l = []
for x in xrange(pixel_width * W, pixel_width * (W + 1)):
for y in xrange(pixel_height * H, pixel_height * (H + 1)):
if x < width and y < height:
l.append(img.getpixel((x, y)))
color = pixelate_mode(mode)(l)
for x in xrange(pixel_width * W, pixel_width * (W + 1)):
for y in xrange(pixel_height * H, pixel_height * (H + 1)):
if x < width and y < height:
img.putpixel((x, y), color)
H += 1
W += 1
return img


And these are some examples applying the program to this image: it has 200×200 pixels, and each square of the image is 10×10 pixels.

pixelate(img, (10, 10), 'mean'), pixelate(img, (5, 5), 'max'), pixelate(img, (2, 2), 'min') and pixelate(img, (1, 1), 'mean') return the same image.

pixelate(img, (11, 11), 'mean') returns this.

Simple optimisation

Instead of doing color = pixelate_mode(mode)(l) in the nested loop, you could compute modefunc = pixelate_mode(mode) once at the beginning of the function then call modefunc(l) as you need it.

Other optimisation

Instead of checking if x and y are in the range, you could just generate them so that they always are in the range :

l = []
for x in xrange(pixel_width * W, min(width, pixel_width * (W + 1))):
for y in xrange(pixel_height * H, min(height, pixel_height * (H + 1))):
l.append(img.getpixel((x, y)))
color = modefunc(l)
for x in xrange(pixel_width * W, min(width, pixel_width * (W + 1))):
for y in xrange(pixel_height * H, min(height, pixel_height * (H + 1))):
img.putpixel((x, y), color)


Also, for a more optimised version, you could compute limits only once :

minx, maxx = pixel_width * W, min(width, pixel_width * (W + 1))
miny, maxy = pixel_height * H, min(height, pixel_height * (H + 1))
l = []
for x in xrange(minx, maxx):
for y in xrange(miny, maxy):
l.append(img.getpixel((x, y)))
color = modefunc(l)
for x in xrange(minx, maxx):
for y in xrange(miny, maxy):
img.putpixel((x, y), color)


List expressions

You could use a list expression to define l. Even better you do not need l as a list at all and could just use a generator expression (Edit: Apparently this does not work for np.array so you have to build the string anyway, may it be with multiple appends or with list comprehension).

minx, maxx = pixel_width * W, min(width, pixel_width * (W + 1))
miny, maxy = pixel_height * H, min(height, pixel_height * (H + 1))
color = modefunc(img.getpixel(x, y)
for x in xrange(minx, maxx)
for y in xrange(miny, maxy))
for x in xrange(minx, maxx):
for y in xrange(miny, maxy):
img.putpixel((x, y), color)


More optimisations

You could compute the x limits outside the nested loop.

Your while loops are actually for loop in disguise and you could make things more efficient by using for W in range(width/pixel_width) or you could use the step argument of range to use the values you'll actually need : for w in range(0, width, pixel_width): (in both cases, there might be an off-by-one issue but I'll let you check and fix).

Final (untested) version of the code

def pixelate(img, (pixel_width, pixel_height), mode):
width, height = img.size
modefunc = pixelate_mode(mode)
for w in range(0, width, pixel_width):
minx, maxx = w, min(width, w + pixel_width)
for h in range(0, height, pixel_height):
miny, maxy = h, min(height, h + pixel_height)
color = modefunc(img.getpixel(x, y)
for x in xrange(minx, maxx)
for y in xrange(miny, maxy))
for x in xrange(minx, maxx):
for y in xrange(miny, maxy):
img.putpixel((x, y), color)
return img