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.

Homework 1 - Drawings

Homework 1 - Milestones

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

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

alt: zero based indexing of x in image

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

(recall) range(n) Function

See 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]

(recall) How for/range Works

alt: for loop var and range(n), sets var to point to each number in turn

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


Goal: Generate x,y Numbers For Image

alt: x,y numbers on image

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

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

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

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

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 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
alt:get_pixel(x,y) returns reference to that pixel

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
x,y grid of pixels for example image width=100 and height=50

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

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. Drawing For X Values

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: Stripe x values are 0 .. 9, 10 pixels wide.

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

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.

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


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

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