Today: image co-ordinates, range, nested range, make drawing to figure out co-ords, using a parameter.
The live image problems today are linked into the notes below, also available in the experimental server sections image-nested (nested loops) and image-shift (x,y coords shift around)
Section begins this week, builds on the image examples shown today.
Today we'll build up a more sophisticated approach - loop over all the x, y coordinates of an image, using nested loops and the range() function.
Have an image with a given width and height. How to generate all the x, y numbers to cover the whole image?
The x and y numbers start at 0 - "zero based indexing". This is an incredibly common scheme within computers, so you'll get used to it. Note: zero based indexing causes the math to come out more cleanly for a bunch of cases .. that's why it's used so widely in the computer.
Here the width is 6, so the x numbers range: 0, 1, 2, 3, 4, 5
The height is 4 so the y numbers range: 0, 1, 2, 3
It's easy to think — well it's width 6 so the rightmost pixel is at x = 6. Nope! In zero based indexing, the last index is 1 less than the number of things — 6 pixels, last pixel is index 5, aka last pixel is at x = (width - 1)
More generally, if you have n
things with zero-based indexing, the first is at 0
and the last is at n - 1
.
Zero based indexing makes the math come out cleaner for some cases, which is why it is used in code. This is not deeply difficult, but it's an easy "off by one error" (OBO) to make. We'll talk about OBO more later. (SAT story about distractor answers that look right but are wrong. What is the rightmost x value if the width is 100?)
range(n)
FunctionSee the Python Guide range
The Python range(n)
function returns a series of numbers:
range(6) -> [0, 1, 2, 3, 4, 5] # UBNI range(2) -> [0, 1] range(1) -> [0]range(n) -> [0, 1, 2 .... n-1]
Here range(6) -> [0, 1, 2, 3, 4, 5]
For loop operation: set the variable (x
in this case) to point to each value in collection. Run the loop body once for each value.
Result - given a collection of N elements, the for loop runs the loop body N times, once for each element. The for loop lets you run a bit of code once for each element in a collection.
Try it on the experimental server interpreter console >>>. (Also available in the Code menu on experimental server pages.)
image.width
and image.height
Our Python image object has properties: image.width
and image.height
Here image.width
is 6 and image.height
is 4
Use range(image.width)
to generate the x
values, and likewise for y
:
range(image.width) -> [0, 1, 2, 3, 4, 5] range(image.height) -> [0, 1, 2, 3]
The range() function is set up to work with zero-based indexing. Here you feed it the width, and get back the x numbers.
Use for loops to loop over the numbers from range():
for x in range(image.width): # x is 0, 1, 2, 3, 4, 5 ... for y in range(image.height): # y is 0, 1, 2, 3 ...
How to combine the two loops to generate all the x,y? Nested Loops - a classic structure.
Say we have an image width 6, height 4. Here are the nested loops to cover all its x,y. To go over an image, the "y" loop is first, and the "x" loop is nested inside it. With this structure, the loops go over the whole image from top to bottom.
for y in range(4): # outer for x in range(6): # inner # use x,y in here
Each run of a loop body is called an iteration. Here is the key rule:
Rule: For one iteration of outer, get all the iterations of inner.
The outer loop does one iteration (e,g, y = 0). Then inner goes through all the x values, x = 0, 1, 2, 3, 4, 5. Then outer does one iteration (y = 1), and inner goes through all the x values again.
y = 0 # outer, y = 0 x = 0, 1, 2, .. 5 # inner, x goes through all y = 1 # outer, y = next value x = 0, 1, 2, .. 5 # inner, x through all again y = 2 # outer, y = next value x = 0, 1, 2, .. 5 # inner, x through all again ...
e.g. y = 0, go through all the x's 0, 1, 2 .. 4, 5. Then for y = 1, go through all the x's again.
The print() function is standard Python and we'll use it more later. It takes one or more values separated by commas value in the parenthesis and prints them out as a line of text.
Run the nested loops in the experimental server (interpreter). You can see the key rule in action - one iteration of the outer loop selects one y number, and for that one y, the inner loop go through all the x numbers:
>>> for y in range(4): for x in range(6): print('x:', x, 'y:', y) x: 0 y: 0 x: 1 y: 0 x: 2 y: 0 x: 3 y: 0 x: 4 y: 0 x: 5 y: 0 x: 0 y: 1 x: 1 y: 1 x: 2 y: 1 x: 3 y: 1 x: 4 y: 1 x: 5 y: 1 x: 0 y: 2 x: 1 y: 2 x: 2 y: 2 x: 3 y: 2 x: 4 y: 2 x: 5 y: 2 x: 0 y: 3 x: 1 y: 3 x: 2 y: 3 x: 3 y: 3 x: 4 y: 3 x: 5 y: 3
Why is y loop first? This way we go top to bottom — y=0, then y=1 and so on. This is the standard, traditional order for code to loop over an image, so we'll always do it this way (and if you encounter image code out in the world someday, it will tend to do it in this order too).
Here is a picture, showing the order the nested y/x loops go through all the pixels - all of the top y=0 row, then the next y=1 row, and so on. This the same order as reading English text from top to bottom.
for y in range(image.height): for x in range(image.width): # use x,y in here
Looking at every part of the nested for loop above is complex. However, the result is simple - loop over all x,y of an image. We will use the nested y/x loop idiomatically this way to look at every pixel in an image, so you can get used to it.
image.get_pixel(x, y)
# get pixel at x=5 y=2 in "image", # can use its .red etc. pixel = image.get_pixel(5, 2) pixel.red = 0
This code works - pulls together all of the earlier topics in a running example.
Here is a version of our earlier "darker" algorithm, but written using nested range() loops. The nested loops load every pixel in the image and change the pixel to be darker. On the last line return image
outputs the image at the end of the function (more on "return" next week). Run it to see what it outputs. Then we'll look at the code in detail.
def darker(filename): image = SimpleImage(filename) for y in range(image.height): for x in range(image.width): pixel = image.get_pixel(x, y) pixel.red *= 0.5 pixel.green *= 0.5 pixel.blue *= 0.5 return image
So in this example, we have the standard y/x loop form that hit every pixel in the image. So these are the loops we'll use below to get all the pixels.
image = SimpleImage(filename)
for y in range(image.height):
for x in range(image.width):
# use x,y in here
# Make pixel b look the same as pixel a
b.red = a.red
b.green = a.green
b.blue = a.blue
Thus far the code has changed the original image. Now we'll create a new blank white "out" image and write changes to that. Here are a few examples of creating a new, blank image.
# 1. Say filename is 'poppy.jpg' or whatever # This loads that image into memory image = SimpleImage(filename) # 2. create a blank white 100 x 50 image, store in variable # named "out" out = SimpleImage.blank(100, 50) # 3. Create an out2 image the same size as the first image out2 = SimpleImage.blank(image.width, image.height) # 4. Create an image twice as wide as the first image out_wide = SimpleImage.blank(image.width * 2, image.height)
In the code below, we have two images "image" and "out" - how to obtain a pixel in one image or the other? The key is which image is before the dot when the code calls get_pixel(). This is the essence of noun.verb function call form. Which image do we address the get_pixel() function call to?
image = SimpleImage(filename) # Original image pixel = image.get_pixel(8, 4) # "pixel" at 8, 4 in original out = SimpleImage.blank(image.width, image.height) # out image pixel_out = out.get_pixel(6, 4) # "pixel_out" at 6, 4 in out image # Could copy red from one to the other pixel_out.red = pixel.red
The same "darker" algorithm, here writing the darker pixels to a separate "out" image, leaving the original image unchanged.
The "return xxx" line returns a completed value back to the caller code. Often the last line of a function. We'll use it in more detail later, but for these examples, it returns our "result" image.
We use the variable names carefully &mdash pixel
points to the pixel in the original image, while pixel_out
points to the pixel in the out image. It's easy to get mixed up about which pixel is which, so give them distinctive names to try to keep things straight.
Demo: try commenting out the return line. What does the function run do now?
Demo: try changing last line from return out
to return image
- what do you see and why?
def darker(filename):
image = SimpleImage(filename)
# Create out image, same size as original
out = SimpleImage.blank(image.width, image.height)
for y in range(image.height):
for x in range(image.width):
pixel = image.get_pixel(x, y)
pixel_out = out.get_pixel(x, y)
pixel_out.red = pixel.red * 0.5
pixel_out.green = pixel.green * 0.5
pixel_out.blue = pixel.blue * 0.5
return out
It's hard to write the get_pixel() line with its coordinates just right doing it in your head. We make a drawing and take our time to get the details exactly right.
Notice that our drawing was not general - just picking width = 100 as a concrete example. A single concrete example was good enough to get our thoughts organized, and then the formula worked out actually was general.
A common form of error in these complex indexing algorithms is being "off by one", like accessing the pixel at x = 100 when x = 99 is correct.
> Aqua 10
For the Aqua 10 problem, take in an original image, and produce an image with a 10 pixel wide aqua stripe on the left, with a copy of the original image next to it, like this:
We'll use 2 nestings to produce the output: One nesting for the aqua rectangle, and one nesting for the copy of the image.
Say the original image is 100 pixels wide. The out image has a 10 wide stripe at the left, with a copy of the original image to its right.
QL What is the width of the stripe at the left?
Q: What is the width of the out image?
Use the drawing to work these out.
A: out image is 110 pixels wide.
image = SimpleImage.blank(filename) out = SimpleImage.blank(image.width + 10, image.height)
Make out image 10 pixels greater in width than original, same height.
How to make aqual color? There is a trick to do it in one line starting with a white pixel. Recall that for a white pixel, RGB are all 255.
# have white pixel # so pixel color is (255, 255, 255) pixel.red = 0 # now pixel color is (0, 255, 255) # aqua!
Blue + green makes aqua! Some day you may find yourself floating in warm, aqua waters .. and you will think back to your love of the RGB color system!
Q: What are the X values for the stripe?
Use drawing to work it out.
A: Stripe x values are 0 .. 9, 10 pixels wide.
Think about standard y/x loops to loop over whole stripe. The vars "x" and "y" in the loop are the coords in the original image.
1. Y values are the height of the original image, so that's just the regular y loop:
for y in range(image.height):
2. The X values are 0..9
. What's the Python to generate that? range(10)
for x in range(10):
Put together it looks like this. Inside the loop, we get the pixel for each x, y, and set it to be aqua.
# Create the 10-pixel aqua stripe
for y in range(image.height):
for x in range(10):
pixel_out = out.get_pixel(x, y)
pixel_out.red = 0
Now to copy the data from the original to the right side of the output.
We will loop over the original image, giving us the x, y values in the original Need to figure out the corresponding X value in the output, call it x_out
. The y values are the same between the original and the output, so we won't worry about those.
To figure this out, look at two points, A and B, in the original image. Where are A and B in the out image? For the x in the original, what is the corresponding x_out in the output?
KEY: x
is in the original, x_out
is in the output
We'll make a little chart, showing the x_out for each x. If you have x in the original image, what's the formula to compute x_out?
point x x_out A 0 -> 10 B 99 -> 109 What's the pattern? x_out = x + 10
That planning goes into the two key lines below (marked "# keys lines").
We get pixel
in the original image at x, y. The we get the corresponding pixel_out
in the out image at x + 10
.
def aqua_stripe(filename): image = SimpleImage(filename) # Create out image, 10 pixels wider than original out = SimpleImage.blank(image.width + 10, image.height) # Create the 10-pixel aqua stripe for y in range(image.height): for x in range(10): pixel_out = out.get_pixel(x, y) pixel_out.red = 0 # Copy the original over - make drawing to guide code here for y in range(image.height): for x in range(image.width): # key lines pixel = image.get_pixel(x, y) pixel_out = out.get_pixel(x + 10, y) pixel_out.red = pixel.red pixel_out.green = pixel.green pixel_out.blue = pixel.blue return out
> Aqua 10
Experiments: Try range(image.height - 40) for the y loop. Try x + 2 for out_pixel. (May do experiments on mirror2 below in the interest of time.)
> Mirror1
def mirror1(filename): image = SimpleImage(filename) # Create out image with width * 2 of first image out = SimpleImage.blank(image.width * 2, image.height) for y in range(image.height): for x in range(image.width): pixel = image.get_pixel(x, y) # left copy pixel_left = out.get_pixel(x, y) pixel_left.red = pixel.red pixel_left.green = pixel.green pixel_left.blue = pixel.blue # right copy # nothing! return out
> Mirror2
This a Nick-favorite example, bringing it all together. This algorithm pushes you to work out details carefully with a drawing as the algorithm is complicated, and the output is also neat.
How do you solve something that looks impossible? Slow down, make a drawing, don't do it in your head.
Mirror2: Like mirror1, but also copy the original image to the right half of "out", but as a horizontally flipped mirror image. So the left half is a regular copy, and the right half is a mirror image. (Starter code does the left half).
I think a reasonable reaction to reading that problem statement is: uh, what? How the heck is that going to work? But proceeding carefully we can get the details right. Do with a drawing, not in your head.
Make a drawing of the image coordinates with concrete numbers, work out what the x,y coordinates are for input and output pixels. We'll go though the whole sequence right here.
Here are 4 points in the original: A: (0, 0) B: (1, 0) C: (2, 0) D: (99, 0) Sketch out where these points should land in the output. What is the x value for pixel_right for each of these?
Try completing drawing with ABCD values. This is a great example of slowing down, working out the details. We start knowing what we want the output to look like, proceed down to the coordinate details.
Sequence: put A, B, C, D on output. What are the numbers for each? Make a chart of the input/output numbers, showing out_x for each input x. What is the general formula for out_x from the pattern?
Here is the drawing with the numbers filled in
def mirror2(filename): image = SimpleImage(filename) out = SimpleImage.blank(image.width * 2, image.height) for y in range(image.height): for x in range(image.width): pixel = image.get_pixel(x, y) # left copy pixel_left = out.get_pixel(x, y) pixel_left.red = pixel.red pixel_left.green = pixel.green pixel_left.blue = pixel.blue # right copy # this is the key spot # have: pixel at x,y in image # want: pixel_right at ??? to write to pixel_right = out.get_pixel(out.width - 1 - x, y) pixel_right.red = pixel.red pixel_right.green = pixel.green pixel_right.blue = pixel.blue return out
> Mirror2
Remove the "- 1" from formula above, so out_x value is one too big. A very common form of Off By One error. What happens when we run it?
Off By One error - OBO - a very common error in computer code. Surely you will write some of these in CS106A. It has its own acronym and wikipedia page.
Here's another variation on the 2-image side by side form, this one with the left image upside down:
> Mirror3
In next lecture, we'll see how to write code in a function with an "n" parameter, specifying how wide part of the output should be. Here is an example of that technique.
> Side N
side_n: The "n" parameter is an int value, zero or more. The code in the function should use whatever value is in n. (Values of n appear in the Cases menu.) Create an out image with a copy of the original image with n-pixel-wide blank areas added on its left and right sides. Return the out image.
We'll start down the path with parameters a little here. A "parameter" is listed within the parenthesis.
def side_n(filename, n):
Each parameter represents a value that comes in to the function when it runs. The function just uses each parameter. We'll worry about where the parameter value comes from later. For today: treat the parameter like a variable that has a value in it and the code simply use each parameter, knowing its value is already set.
For example, we have the "n" parameter to side_n(), specifying how wide the blank space is on each side. What is the line to make the new blank image? How wide should it be. The width of the out image is the width of the original, plus two n-wide areas. So the whole width is image.width + 2 * n
out = SimpleImage.blank(image.width + 2 * n, image.height)
Notice how the n
is just in the code. This works, because each parameter is set up with the proper value in it before the function runs.
def side_n(filename, n): image = SimpleImage(filename) # Create out image, 2 * n pixels wider than original out = SimpleImage.blank(image.width + 2 * n, image.height) # Copy the original over - shifting rightward by n for y in range(image.height): for x in range(image.width): pixel = image.get_pixel(x, y) pixel_out = out.get_pixel(x + n, y) # shift by n pixel_out.red = pixel.red pixel_out.green = pixel.green pixel_out.blue = pixel.blue return out