Back to Contents

Project 4: Photo Finish

Earlier, we drew pictures by having a turtle draw lines. In this chapter, we consider another kind of image: photographic ones, which are made up of millions of little ‘pixels‘ (picture elements) in a square grid. Here is such a picture (in colour in the electronic version of this book, in black and white in the printed version):

image

This picture has 200 rows and 200 columns, making in total 40,000 pixels, each of which is an (r, g, b) triple where red, green, and blue range from 0 (no light of that colour) to 255 (full light of that colour) inclusive. You can download this picture from the book’s website, or find one of your own. It must have a file name ending .png and should not have any transparent elements (we will see how to check this soon).

This chapter, unlike every other chapter in this book, requires installing some external software, namely the Pillow library for Python. Installation instructions depend upon your computer, and may change over time, so the author can only direct you to the internet. You will know you have it installed properly when the following does not give an error:

Python
>>> from PIL import Image

This is a version of the import construct we have not yet seen. It provides access to just the Image sub-module of the Pillow library. Now we can open the image:

Python
>>> i = Image.open('rabbit.png')

The Pillow module has a built in method to show an image on screen. How it is shown depends on your system – typically it is opened in whichever program is set as the default program to show PNG files.

Python
>>> i.show()

The method load on the image gives us access to the actual pixels, and size gives us the number of rows and columns. We get the value of a pixel at a given coordinate by writing p[x, y] where p is the result of load. Let us make the image greyscale by averaging the red, green, and blue values for each pixel:

image

If you accidentally chose a PNG file with transparency, you will see an error similar to this:

Python
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
ValueError: too many values to unpack (expected 3)

We can save it to a new file:

Python
>>> i.save('greyrabbit.png')

We might like to write several functions of this type, so it makes sense to split up the idea of processing all the pixels in-place from the calculation done on each pixel. Let us write a grey function:

image

Now we can write process_pixels_in_place which takes a function such as grey and an image, and processes each pixel using the function:

image

Now we can use it like this:

Python
>>> process_pixels_in_place(grey, i)

Question 1 Write functions to increase or decrease the brightness and contrast of an image. Brightness may be achieved by simple addition of each component of each pixel by an appropriate factor, and contrast by multiplication.

Question 2 Write functions to flip an image vertically or horizontally, and to rotate an image by 180 degrees, all in-place.

We have loaded one image, modified it, and saved it. But how do we make a new image, or combine images? Let us write a function to add a border to an image, like a picture frame:

image

The function border adds a border of a given width and colour to an image, returning a new image. The original is unaltered. We use the Image.new method to build a new image of larger dimensions, filled with the frame colour. Then, we copy the pixels one-by-one. Now we can use it:

Python
>>> bordered = border(i, 20, (150, 150, 150))

Here is the result:

image

We can blur an image, to make it look out of focus, by making each pixel the average of its eight surrounding pixels and itself:

image

Here we have used the copy method to make, and thus return, a new image of the same size. Now we can use it a few times, making sure to begin with an image bordered in white, so that there is room for the blurring:

Python
>>> white_bordered = border(i, 20, (255, 255, 255))
>>> x = blur(blur(blur(white_bordered)))
>>> x.save('blurred.png')

Here is the result:

image

Question 3 Rewrite the blurring to work in-place. Is the result appreciably different?

Question 4 How wide does the border have to be for any given number of blurring operations? Implement a version which uses only the border required.

We have only looked at still pictures so far: Pillow has some facilities for dealing with basic moving pictures in the form of the animated GIFs we are all so familiar with from the internet. We are going to fade our image out to white over the course of a number of ‘frames‘ making up a short animated GIF.

Since we will need to keep multiple images, each processed differently, we shall need a version of our pixel-processor which does not work in-place, but rather returns a new, processed image:

image

Here is the function to fade towards white by a given factor from 0 to 100. With a pen and paper, or your computer, can you verify it is correct?

image

Now we can collect images for each level of fade from 100 down to 0 in steps of 5:

image

We use the function to build the faded images from our original, and save the animated GIF:

Python
>>> images = make_images(i)
>>> images[0].save('fade.gif',
                   save_all=True,
                   append_images=images[1:],
                   duration=100,
                   loop=0)

The mechanism to save the GIF is not intuitive: it is a method on the first image, listing the rest of the images. The duration is the number of milliseconds between frames, that is to say one tenth of a second. Here are frames 1, 10, and 15:

image image image

Question 5 Produce a GIF of your picture, being blurred repeatedly, perhaps 100 times. You will notice that the result is a little grainy – GIFs can have only 256 colours in them, for historical reasons.