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

Strategy Note: Detail Oriented

-

Last Time - Red Channel For Loop

alt: red channel image

Last time, we had example for-loop code to compute the red-channel of an image.

Loop operation: the loop sets the variable "pixel" to point to a different pixel for each run of the lines in the loop.

For the programmer it's simple: I want to do XYZ to each pixel in this image. Place the code to do XYZ inside the loop, it runs once for each pixel in the image.

for pixel in image:
    pixel.green = 0
    pixel.blue = 0

alt: for loop, sets variable to point to each element in the collection, 1 for each iteration of the loop

> The image1 section has problems like this, using a for-loop to change every pixel in an image.

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.

X and Y Coordinate Numbers

Say we have an image that is 6 pixels wide by 4 pixels high. Here we can see the range of x and y coordinate numbers. The x numbers grow to the right, the y numbers grow going down - computer systems use this system all over, akin to numbering the lines of text going down a page.

Here is a diagram of an image of 6 pixels width and 4 pixels height. The origin (0, 0) is at the upper left. The x numbers grow to the right, unusually, the y numbers are 0 at the top and grow going down. Sorry this is a little strange, but it is a super common system within computers and you can get used to it. This system is akin to numbering the lines of text on a page from top to bottom.

alt: zero based indexing of x in image

Zero Based Indexing

The x and y numbers at 0 - "zero based indexing". This is an incredibly common scheme within computers, so you'll get used to it.

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

range(n) Function

See the Python Guide range

The Python range(n) function returns a series of numbers:

range(10) -> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
range(6)  -> [0, 1, 2, 3, 4, 5] 
range(3)  -> [0, 1, 2]               # UBNI

range(n) -> [0, 1, 2 .... n-1]

What Does This for/range Do?

for x in range(6):
    print('in loop', x)

Combines for loop and range() - how does that work?

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.

Demo - for/range in Interpreter >>>

The "interpreter" lets us type code Python interactively to see what it does. Very handy to try out ideas. The interpreter prompt is >>> and we type code there. Python evaluates what we type, prints the result.

The experimental server has an interpreter >>> for this as demo or exercise.

>>> 1 + 2 * 3       # We type, Python responds
7
>>>
>>> print('Hello')  # print() demo
Hello
>>>
>>> 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
>>>

Observe: range(10) generates the numbers 0..9. The variable x points to each number from the collection, one number per run of loop body. The print() function prints out each one on a line, and we'll learn more about that later.

Demo: try a bigger number. Use the up-arrow (!!). "Sibling mode" - change the text to something like 'Too much Python?' and then use the number 1000.

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

# Create original image
image = SimpleImage(filename)

# pixel points to pixel (8, 4) in original image
pixel = image.get_pixel(8, 4)


# Create out image, same size
out = SimpleImage.blank(image.width, image.height)

# pixel_out points to pixel (6, 4) in out image
pixel_out = out.get_pixel(6, 4)

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

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

Aqua 10 - (skipping in lecture)

This shows writing pixels in the output at a different location than the input.

> Aqua 10

For the Aqua 10 problem, 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

1. Make out image - 10 pixels wider than original

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

2. How To Make Aqua Color?

3. How To Loop Over the Stripe

    # 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

Drawing to think about coordinates...

alt: 10 pixel stripe on left

4. Loop To Copy Over Original - Drawing!

How to copy the data from the original to the right side of the output? Need to think about the x,y values, which are not the same in the two images.

alt: look at points A and B in original and out images

For x,y in original, what is the corresponding x,y in out?

Make a little chart (here "x" means in the original image)

pt   x     x_out
A    0     10
B    99    109

What's the pattern?
x_out = x + 10

Now we can write the key get_pixel() line below, to figure pixel_out for each original pixel.

Aqua 10 Solution

def aqua_stripe(filename):
    """
    Create an out image 10 pixels wider than the original.
    (1) Set an aqua colored vertical stripe 10 pixels wide
    at the left by setting red to 0.
    (2) Copy the original image just to the right
    of the aqua stripe. Return the computed out image.
    """
    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):
            pixel = image.get_pixel(x, y)
            pixel_out = out.get_pixel(x + 10, y)  # key line
            pixel_out.red = pixel.red
            pixel_out.green = pixel.green
            pixel_out.blue = pixel.blue
    return out

Experiments: try +11 instead of +10 - get bad-coord exception, Try +9, and image shifted slightly to left, can try the diff-stripes slider. (Can also try errors in mirror2 below.)


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.


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

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) Side N - Parameter

> 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

(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) Keyboard Shortcuts

See Python Guide: Keyboard Shortcuts

These work on the experimental server and other places, including the PyCharm tool we'll show you on Fri. Ctrl-k works in GMail .. so satisfying!