Loops and coordinate transformations
Today we are reviewing concepts to help you better understand some of what we’ve been doing with images.
Looping in one dimension
First, let’s look at looping in one dimension:
from byuimage import Image
image = Image.blank(100, 100)
x = 30
for y in range(image.height):
pixel = image.get_pixel(x, y)
pixel.red = 255
pixel.green = 0
pixel.blue = 0
image.show()
We create a blank image that is 100x100 pixels, set x = 30
, and then loop
through just the rows of the image, using the for loop. This creates a thin red
line that starts at x=30
and covers all of the y
values from 0 to the image
height.
Now let’s copy-and-paste that code, and change x
so that it increments from 30
to 34:
x = 30
for y in range(image.height):
pixel = image.get_pixel(x, y)
pixel.red = 255
pixel.green = 0
pixel.blue = 0
x = 31
for y in range(image.height):
pixel = image.get_pixel(x, y)
pixel.red = 255
pixel.green = 0
pixel.blue = 0
x = 32
for y in range(image.height):
pixel = image.get_pixel(x, y)
pixel.red = 255
pixel.green = 0
pixel.blue = 0
x = 33
for y in range(image.height):
pixel = image.get_pixel(x, y)
pixel.red = 255
pixel.green = 0
pixel.blue = 0
x = 34
for y in range(image.height):
pixel = image.get_pixel(x, y)
pixel.red = 255
pixel.green = 0
pixel.blue = 0
Now we get a thicker red line that is five pixels wide, covering x = 30 ... 34
and all the y
values.
Looping in two dimensions
This is why we need nested for loops! Instead of having to copy-and-paste our
code each time, we can instead have one loop to cover the x
values and one to
cover the y
values:
for x in range(30,34):
for y in range(image.height):
pixel = image.get_pixel(x, y)
pixel.red = 255
pixel.green = 0
pixel.blue = 0
We will get the same thicker red line:
In this case, we will cover the pixels in this order:
- (30, 0)
- (30, 1)
- (30, 2)
- …
- (30, 99)
- (31, 0)
- (31, 1)
- (31, 2)
- …
- (34, 99)
In other words, we are going column-by-column, by fixing x
, and then doing all
the y
values, and then going to the next x
.
It makes little difference if we loop over the y
values first:
for y in range(image.height):
for x in range(30,34):
pixel = image.get_pixel(x, y)
pixel.red = 255
pixel.green = 0
pixel.blue = 0
We will get the same image, we just cover the pixels in a different order:
- (30, 0)
- (31, 0)
- (32, 0)
- (33, 0)
- (34, 0)
- (30, 1)
- (31, 1)
- (32, 1)
- …
- (34, 99)
In other words, we are going row-by-row, by fixing y
, and then doing all the
x
values, and then going to the next y
.
Writing functions
Anything we have done so far we can put into a function:
from byuimage import Image
def draw_bar(image):
for x in range(30, 34):
for y in range(image.height):
pixel = image.get_pixel(x, y)
pixel.red = 255
pixel.green = 0
pixel.blue = 0
my_image = Image.blank(100, 100)
draw_bar(my_image)
my_image.show()
We will get the exact same result:
A very impressive demo so far!
Generalizing
You can control the height of the bar as well:
def draw_bar(image):
for x in range(30, 34):
for y in range(20, 80):
pixel = image.get_pixel(x, y)
pixel.red = 255
pixel.green = 0
pixel.blue = 0
This gets us a shorter line:
Now, take each of those hard-coded values in the for loops and turn them into parameters for the function:
from byuimage import Image
def draw_bar(image, x_start, x_end, y_start, y_end):
for x in range(x_start, x_end):
for y in range(y_start, y_end):
pixel = image.get_pixel(x, y)
pixel.red = 255
pixel.green = 0
pixel.blue = 0
my_image = Image.blank(100, 100)
draw_bar(my_image, 30, 60, 40, 70)
my_image.show()
Notice that we created four parameters, x_start
, x_end
, y_start
, and
y_end
, one for each of the starting and stopping points used in the range()
function.
Now we can draw a bar of whatever shape we want, wherever we want in the image!
It is not a big stretch from here to pass in the colors as well:
def draw_bar(image, x_start, x_end, y_start, y_end, red, green, blue):
for x in range(x_start, x_end):
for y in range(y_start, y_end):
pixel = image.get_pixel(x, y)
pixel.red = red
pixel.green = green
pixel.blue = blue
my_image = Image.blank(100, 100)
draw_bar(my_image, 30, 60, 40, 70, 150, 20, 100)
my_image.show()
Coordinate transformations
You might prefer to pass in the width and height of the bar instead of having to use coordinates to indicate where the bar stops. You could easily do this by changing the parameters of the function:
def draw_bar(image, x_start, y_start, width, height, red, green, blue):
Doing this requires understanding coordinate transformations.
We can look at the bar separately, and think of it as starting at a coordinate
of (0, 0)
in the top left corner, with a width and height. That means we can
loop over all of its pixels with:
for x in range(width):
for y in range(height):
But now when we want to get the pixel to draw on the image, we need to transform the coordinates if the bar into the coordinates of the image:
for x in range(width):
for y in range(height):
pixel = image.get_pixel(x_start + x, y_start + y)
So our complete drawing function would be:
def draw_bar(image, x_start, y_start, width, height, red, green, blue):
for y in range(height):
for x in range(width):
pixel = image.get_pixel(x_start + x, y_start + y)
pixel.red = red
pixel.green = green
pixel.blue = blue
Alternatively, we can start with the nested for loops looping over pixels in the image directly:
for x in range(x_start, x_start + width):
for y in range(y_start, y_start + height):
pixel = image.get_pixel(x, y)
This allows us to use (x, y)
when getting the pixel.
def draw_bar(image, x_start, y_start, width, height, red, green, blue):
for x in range(x_start, x_start + width):
for y in range(y_start, y_start + height):
pixel = image.get_pixel(x, y)
pixel.red = red
pixel.green = green
pixel.blue = blue
my_image = Image.blank(100, 100)
draw_bar(my_image, 30, 30, 30, 10, 150, 100, 20)
my_image.show()
Either way, we should be able to get the same bar drawn:
We’ve minted a gold bar!
Making borders
Now that we have a function that can draw bars at an arbitrary location, we can use it to draw all the borders of an image:
def make_borders(image, thickness, red, green, blue):
# draw top bar
draw_bar(image, 0, 0, image.width, thickness, red, green, blue)
# draw bottom bar
draw_bar(image, 0, image.height - thickness, image.width, thickness, red, green, blue)
# draw left bar
draw_bar(image, 0, 0, thickness, image.height, red, green, blue)
# draw right bar
draw_bar(image, image.width - thickness, 0, thickness, image.height, red, green, blue)
first_image = Image.blank(100, 100)
make_borders(first_image, 30, 255, 0, 0)
second_image = Image.blank(800, 300)
make_borders(second_image, 10, 150, 20, 100)
first_image.show()
second_image.show()
Run this code and see what you get!
Copying images
We use the same concept of coordinate transformation when copying an image.
Once you write a function to do this:
def copy_image(new_image, original_image, x_start, y_start):
then you can use draw_bar()
and copy_image()
to make all kinds of new
images: