Image Processing and Nested For Loops¶

CS 65: Introduction to Computer Science I¶

Loading an image file in Python¶

Working with images in Python requires a module like the PIL (Python Imaging Library). To get this installed, you will need to install the pillow package.

Here's some sample code for loading and displaying an image.

In [1]:
from PIL import Image

with Image.open("griff.jpg") as griff_image:
    griff_image.show()

Demo: Let's see what this looks like in Thonny.

Image Basics¶

Digital images are made of a grid of picture elements called pixels.

Each pixel is stored in the computer as three numbers: a red, a green, and a blue value representing the amount of each primary color of light needed to make that color. These values usually go between 0 and 255.

Why 255?

Numbers are represented in a computer's memory in base-2, or binary notation which is made up of 0s and 1s (binary digits or bits) like 00100001 (33) or 11011100 (220). It's common to group bits into groups of 8 called a byte, and the biggest number you can represent with 8 bits is 11111111 (255).

PIL representation of images¶

an image object in Python has several pieces of data and methods associated with it, and they can do a lot of sophisticated things. For the full reference on using the module, see https://pillow.readthedocs.io/en/stable/

However, we'll use just a few, simple things that will allow us to write custom code that manipulates images.

We can find the image's size with the size data attribute of our image, and we can load all the pixels into a variable using load().

In [2]:
from PIL import Image

with Image.open("griff.jpg") as griff_image:
    #griff_image.show()
    
    print(griff_image.size)

    pixels = griff_image.load()
    
    print(pixels[0,0])
(732, 412)
(196, 221, 243)

Notice: images store a lot of their information as tuples.

size is a tuple that tells you the number of pixel wide and tall an image is (width,height)

pixel[0,0] is the upper-left pixel in the image - the three values are the red, green, and blue amounts that make up the color stored there

pixel[25,50] is the pixel in the column 25 (the 26th column from the left) and row 50 (the 51st row from the top)

You can't change the tuple already in an image

In [3]:
pixels[25,50][0] = 100
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-221d531f21c1> in <module>
----> 1 pixels[25,50][0] = 100

TypeError: 'tuple' object does not support item assignment

but you can overwrite it with a whole new tuple

In [4]:
pixels[25,50] = (0,0,0) #overwriting pixel 100,200 with black
griff_image.show()
In [5]:
griff_image
Out[5]:

Using loops for image processing¶

If I want to loop accross an entire row of an image, I could use size[0] (the width - the number of columns) to figure out how many iterations of the loops to run.

In [6]:
#loop through every column in the image
for p in range(griff_image.size[0]):
    pixels[p,50] = (0,0,0) #change the pixel at this column, row 50 to black
    
griff_image.show()
In [7]:
griff_image
Out[7]:

and you could do a column similarly

In [8]:
#loop through every row in the image
for p in range(griff_image.size[1]):
    pixels[25,p] = (255,255,255) #change the pixel at this row, column 25 to white
    
griff_image.show()
In [9]:
griff_image
Out[9]:

Saving the image¶

To save the image, run the save() method.

In [10]:
griff_image.save("griff_with_lines.jpg")

We should now have a new image saved in the same directory as our .py file.

If we wanted a thicker line, we might write

In [11]:
from PIL import Image

with Image.open("griff.jpg") as griff_image:
    pixels = griff_image.load()
    
for p in range(griff_image.size[0]):
    pixels[p,50] = (0,0,0) #change the pixel at this column, row 50 to black
    pixels[p,51] = (0,0,0)
    pixels[p,52] = (0,0,0)
    pixels[p,53] = (0,0,0)
    pixels[p,54] = (0,0,0)
    pixels[p,55] = (0,0,0)
    pixels[p,56] = (0,0,0)
    pixels[p,57] = (0,0,0)
    pixels[p,58] = (0,0,0)
    pixels[p,59] = (0,0,0)
    
griff_image.show()
In [12]:
griff_image
Out[12]:

Hopefully by now you're thinking "Hey, I could do that with another loop!"

And indeed you can! You can put a loop inside of another loop - this is called a nested loop.

In [13]:
from PIL import Image

with Image.open("griff.jpg") as griff_image:
    pixels = griff_image.load()
    
for p in range(griff_image.size[0]):
    for r in range(50,60):
        pixels[p,r] = (0,0,0)
    
    
    
griff_image.show()
In [14]:
griff_image
Out[14]:

The entire inner loop runs through all its iterations once for every iteration of the outer loop.

If the inner loop runs 10 iterations, and the outer loop runs 732 iterations, then the total number of pixel assignments is $732*10 = 7320$.

Looping through every pixel in the whole image¶

Outer loop: run through all the columns - there are griff_image.size[0] of them

Inner loop: run through all the rows - there are griff_image.size[1] of them

For this one, let's see what happens when we change all the pixels so that every color is the average of all three colors.

In [ ]:
from PIL import Image

with Image.open("griff.jpg") as griff_image:
    pixels = griff_image.load()
    
    
#in class we'll write a nested for loop here

griff_image.show()
In [15]:
#here is the solution
from PIL import Image

with Image.open("griff.jpg") as griff_image:
    pixels = griff_image.load()
    
for c in range(griff_image.size[0]):
    for r in range(griff_image.size[1]):
        red = pixels[c,r][0]
        green = pixels[c,r][1]
        blue = pixels[c,r][2]
        
        average_pixel_color = (red+green+blue)//3
        
        pixels[c,r] = (average_pixel_color,average_pixel_color,average_pixel_color)
    
    
    
griff_image.show()
In [16]:
griff_image
Out[16]:

You can play with the color values to make lots of other filters

In [23]:
from PIL import Image

with Image.open("griff.jpg") as griff_image:
    pixels = griff_image.load()
    
for c in range(griff_image.size[0]):
    for r in range(griff_image.size[1]):
        red = pixels[c,r][0]
        green = pixels[c,r][1]
        blue = pixels[c,r][2]
        
        
        pixels[c,r] = (red,green,blue*2)
    
    
    
griff_image.show()
In [24]:
griff_image
Out[24]:

Flipping the image horizontally¶

Let's talk about a transformation that requires us to move some pixels around.

griffpixelcircle.jpg

In [ ]:
from PIL import Image

with Image.open("griff.jpg") as griff_image:
    pixels = griff_image.load()
    

#let's work on flipping the image horizontally
    
    
griff_image.show()
In [25]:
#horizontal flip solution
from PIL import Image

with Image.open("griff.jpg") as griff_image:
    pixels = griff_image.load()
    
for c in range(griff_image.size[0]//2):
    for r in range(griff_image.size[1]):

    
        leftside = pixels[griff_image.size[0]-1-c,r]
        pixels[griff_image.size[0]-1-c,r] = pixels[c,r]
        pixels[c,r] = leftside
    
    
griff_image.show()
In [26]:
griff_image
Out[26]: