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.
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.
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).
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().
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])
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
pixels[25,50][0] = 100
but you can overwrite it with a whole new tuple
pixels[25,50] = (0,0,0) #overwriting pixel 100,200 with black
griff_image.show()
griff_image
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.
#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()
griff_image
and you could do a column similarly
#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()
griff_image
To save the image, run the save() method.
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
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()
griff_image
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.
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()
griff_image
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$.
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.
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()
#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()
griff_image
You can play with the color values to make lots of other filters
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()
griff_image
Let's talk about a transformation that requires us to move some pixels around.
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()
#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()
griff_image