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.
We'll work quite a few examples today, really getting into how image loops work. We will do similar material in section. You may want to review these examples later, or when you start homework-2, to solidfy the details vs. this rather quick pass in lecture.
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?
alt: x,y numbers on image
(picking up from lecture-3)
The pixels in an image are numbered starting with 0 - "zero based indexing". This is an incredibly common scheme within computers, so you'll get used to it. Zero based indexing makes the math come out cleaner for some cases, which is why it is used in code.
Below is an image which is 6 pixels wide, i.e. its width is 6. Look at the topmost row of pixels.
The leftmost pixel is at x=0, the next pixel to its right is x=1, and so on up to x=5 for the last, rightmost pixel.
It's easy to think that since the width 6, the rightmost pixel is at x = 6. Nope! In zero based indexing, the last index is 1 less than the number of things — if there are 6 pixels, the last pixel is at index 5. In other words, the rightmost pixel in an image is at x = (width - 1)
alt: zero based indexing of x in image
6 pixels -> index numbers 0..5
10 pixels -> index numbers 0..9
More generally, if you have n
things with zero-based indexing, the first is at 0
and the last is at n - 1
.
for x in range(image.width): print('in loop', x)
We'll use a loop like this to loop over an image. We'll look at the components in this code to understand what it does.
range(n)
Function (Memorize)See the Python Guide chapter: Range
range(n) returns the series of numbers: 0 .. n-1
. The range(n) function is used constnatly in Python programs, so you will need to memorize it.
range(6) -> [0, 1, 2, 3, 4, 5] # UBNI, "parameter"
range(3) -> [0, 1, 2]
range(10) -> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
range(n) -> [0, 1, 2 .... n-1]
Start at 0, go up to but not including N
Parameter - recall that for a function call, information called "parameters" can be passed within the parenthesis for use by the function. Here the n
parameter to range(n) tells it what number to go up to. e.g. in range(6)
we would say "we pass 6 into range".
Look at this example in detail.
# for-loop with range(n): # loop over the numbers for x in range(10): print('in loop', x)
First, the range(10)
expression produces the sequence of numbers [0, 1, 2, .. 9]
.
range(10) -> [0, 1, 2, 3, .. 9]
The number sequence has a red underline in the drawing below. The for-loop takes over the variable x
, pointing it to the number 0 for the first run of the loop body (aka first "iteration"). The function print('in loop', x)
prints 'in loop 0'
. For next iteration, the for-loop sets x
to point to the number 1, and runs the body again. This continues, running the loop body once for each number, until the last iteration with x
pointing to 9.
alt: for loop x points to 0, then 1, then 2, ...
Demo (or you can try it). The print(xx) function in this context just prints out what is passed to it within the parenthesis. Normally we indent by 4 spaces, but it's ok to just indent with a different number of spaces in this temporary, on-the-fly context.
>>> for x in range(10): print('in loop:', x) in loop: 0 in loop: 1 in loop: 2 in loop: 3 in loop: 4 in loop: 5 in loop: 6 in loop: 7 in loop: 8 in loop: 9
1. We see that the for loop works with range()
, running the body once for each number.
2. Try different numbers as the parameter passed in to range(). Use up-arrow in the interpreter to recall previously typed lines, then hit the return key to run the edited version - a great time saver. (laziness feature FTW!).
alt: x,y numbers on image
image.width
and image.height
Image has attributes reflecting its width and height
Width of image -> image.width (e.g. 6) Height of image -> image.height (e.g. 4)
Feed these numbers into range() to generate all the coordinates.
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) -> range(6) -> [0, 1, 2, 3, 4, 5] range(image.height) -> range(4) -> [0, 1, 2, 3]
It is no accident that this works out perfectly — the range() function is set up to work with zero-based indexing, where you give i tth e 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.
>>> interpreter on experimental server
The print() function is standard Python and we'll use it more later. It takes one or more values separated by commas value in its parenthesis and prints them out as a line of text.
Run the nested loops in the interpreter on the experimental server (can copy/paste from below). 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 or any 2-d structure, 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.
alt: nested loop order, top row, next row, and so on
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 this same nested y/x loop idiomatically many times to look at every pixel in an image, so you can get used to it.
How do we obtain a pointer to an individual pixel?
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
alt: make two pixels look the same
# 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
alt: darker example with image and out
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
We'll show the steps to work out each one.
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:
alt: 10 pixel stripe on left
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.
alt: original is 100 pixels wide, out is 110 wide
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.
alt: original is 100 pixels wide, out is 110 wide
A: The stripe is 10 pixels wide, so its x values are: 0 .. 9
Q: What for-loop makes those?
# Loop x over the values 0..9
for x in range(10):
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
alt: original is 100 pixels wide, out is 110 wide
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 i.e. pixel at x in original image is at x + 10 in output image
For example, for a pixel at x=25 in the original image, that pixel would be at 35 in the output image.
The planning show above 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) # key - orig at x pixel_out = out.get_pixel(x + 10, y) # key: out x + 10 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
alt: pixel in original, pixel_left in out image
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: Given an input image, create an output image twice as wide. Place a copy of the original in the left half. Place a horizontally flipped copy of the original in the right half. (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?
alt: figure dest x,y for source points A B C D
Here is the drawing with the numbers filled in
alt: figure dest x,y for source points A B C D
orig-image x x_out A: 0 199 B: 1 198 C: 2 197 D: 99 100Looking at above, what's the pattern? Work out that the formula is x_out = 199 - x
Guess that 199 in general is: out.width - 1 x_out = out.width - 1 - x
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