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.

Many Examples 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.

Strategy Note: Detail Oriented


Today: Loop Over All x, y Coordinates

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.

Goal: Generate x,y Numbers For Image

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


Zero Based Indexing

(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.

What Does This Loop Do?

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.

1. 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".

2. For Range Loop

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, ...

(optional) Hack/Demo: Interpreter in Experimental Server

> interpreter >>>

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!).


Goal: Generate x,y Numbers For Image

alt: x,y numbers on image

1. Use range() With 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.

2. Use for-x and for-y Loops

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 for-x and for-y Loops?

How to combine the two loops to generate all the x,y? Nested Loops - a classic structure.

Nested Loops

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

How Does Nested Work?

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.

Nested Loop In Interpreter>>>

>>> 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?

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).

Nested Loop Visualization

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

Good News - Looks Complicated, but..
Idiomatic Loop Over All x, y

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 To Access a Pixel?

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

Example: Darker-Nested

This code works - pulls together all of the earlier topics in a running example.

> Darker Nested

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

Observe Darker Nested Observations

Demos With Darker Nested

Idiomatic y/x Loops

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

How to Make 2 Pixels Look The Same?

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

How to Create a New, Blank Image?

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)

Dealing With Two Images - getPixel()

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

Example: Darker Out

> Darker 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

Now We'll Work Some Hard Problems

We'll show the steps to work out each one.

Strategy - Make a Drawing

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.

Concrete Numbers

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.

Off By One, OBO

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 Example

> 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.

1. What are the Widths?

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.

2. Make out image - 10 pixels wider than original

image = SimpleImage.blank(filename)
out = SimpleImage.blank(image.width + 10, image.height)

Make out image 10 pixels greater in width than original, same height.

3. How To Make Aqua Color?

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!

4. What x Values For Stripe? For-loop To Make Those?

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):

5. Loops To Make The Aqua Stripe

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

6. Think About Shifted Image X

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.

Aqua 10 Solution

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.)


(optional) Example - Mirror1

> 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 - Appears Impossible

> 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).

Mirror2 Strategy

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?

Sketch out ABCD Values

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

ABCD Table Solution
 orig-image
 x x_out
A: 0 199
B: 1 198
C: 2 197
D: 99 100

Looking 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

mirror2 Solution Code

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

Debugging: Put in a bug

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 - Classic!

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.

(Optional) Extra Practice - Mirror3

Here's another variation on the 2-image side by side form, this one with the left image upside down:

> Mirror3

(optional) Side N - Parameter

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.

def / Parameter

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.

Side N - Blank Image

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.

Side N Solution

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

AltStyle によって変換されたページ (->オリジナル) /