CS101 - Homework 7

Due: 2018-04-18 11:59p

Objectives

Prelab

Before coming to lab on Thursday/Friday find one or more color images that you will want to work with. Edit them so that they have no more than 800 pixels on the longest side (any simple photo viewer/editor such as "Preview" will be sufficient). Save your resized color image(s) to the same folder where your Python program will be. Create your file username_hw7.py and copy the justblue() function below into your file.

[5 points] Power

Write a function called power(L, exponent), which takes in a list of numbers L and returns a new list where each value of the original list has been raised to exponent. Use a list comprehension instead of a loop.

[5 points] Average

Write a function called average(L), which returns the average (arithmetic mean) of a list of numbers L. Use a loop.

[10 points] Pig Latin

Write a function called pigLatin(s) that takes in a string and returns a new string with every word converted to pig latin. We will use three rules for converting a word to pig latin:

  1. If the word is one character long, leave it alone
  2. If the word starts with a vowel (a,e,i,o,u), just add ‘yay’ to the end of the word. (e.g., “apple” => “appleyay”)
  3. Otherwise, the first letter of the word is moved to the back and ‘ay’ is appended to the end (e.g., “dalek” => “alekday”)

Do not worry about punctuation and assume you will be given actual words.

Use the string function split(), which will convert a string into a list. Create a new empty list to collect your answer in. Convert each item in the list to pig latin, appending each new pig latin word onto the end of the answer list. Then convert the answer list back into a string using the join() function. The syntax for the join() function is a little weird. You call it on the string that you want to use as glue and pass it the list of words to glue together.

>>> ' '.join(["this","is", "a", "string"])
'this is a string'

Working with images

This week, we will implement some image transformation functions. If you don't yet have a resized image of your own to work with, for now you can use either sample image 1 or sample image 2.

Put the image in the same directory as your Python file.

Create your new Python file and then put this line at the top:


from PIL import Image

This imports the library we will be using to manipulate the images. The library is called Pillow (Pillow is an updated version of an older project called PIL, thus the name). Here is the documentation.

This is a different import format than we have used previously. This imports a single item out of a library (in this case Image). The following command opens an image from the same directory (passed in as a string) and returns an image object. The PIL module can open a variety of different image formats. Generally, the images you have available will be .jpg, .png, or .gif files, all of which can be opened.

Run this file, despite the fact that it doesn’t appear to do anything. It will import the Pillow library and set the working directory so we can find your image.

In the Python console, type the following line (updating the string containing the file name as appropriate).


image = Image.open('statue.jpg')

This will open the file and store a representation of the contents in a variable called image (you can, of course, use any variable name you like).

To see the image, you can call its show() function.


image.show()

When you are ready to save the image back out to the file system, you can call the save() function like this:


image.save('statue_mirror.png')

PIL will figure out what format to save the file based on the suffix you supply the name. The file will turn up in the same working directory as your input images and source files. Save as ‘.png’ rather than ‘.jpg’, because PIL’s default jpg processing looks horrible.

Our style of working with media will be that we open images on the console, pass them into functions that return new images, and then show or save the results.

Example: Isolate the blue channel

As we discussed in class, every pixel in a color image is made up of red, green, and blue data. For our first function we are going to isolate the blue channel by setting the values associated with the other channels to 0.

Here is the function (we'll walk through it together in class, but you can copy it to your file to use as a template if you like).


def justBlue(image):
    """Return a new image containing just the blue values from an image.

    The input should be a PIL image object created using something like
    image = Image.open(filename), where filename is a string.
    """
    # create a copy of the original image so we don't lose the original
    newImage = image.copy()

    # create our 2D structure of pixels from the image so we can work with individual pixels
    pixels = newImage.load()

    # get the bounding box -- this is returned as a tuple and we use some python
    # magic to automatically break the tuple up into four variables
    minX,minY,width,height = image.getbbox()

    # iterate over every pixel in the image
    for y in range(height):
        for x in range(width):
            # get the rgb value of the pixel (as a tuple in the order (red, green, blue))
            rgb = pixels[x,y]

            # set the pixel to a new RGB value (in this instance, we keep the original
            # blue value and set the red and green to 0)
            pixels[x,y] = (0,0, rgb[2])

    # return the new image
    return newImage

Run this on your image and use show() to look at the result (no need to save this one).


>>> blueImage = justBlue(image)
>>> blueImage.show()

Example: Grayscale

You get shades of gray when all three color channels have the same value. So, to convert an image to grayscale, you need to visit each pixel, take the three color channels and combine them to get the a value we call the luminance of the pixel. You can then take this value and use it for all three color channels to get a grayscale image.

We start by calculating the average (i.e., add the three channels together and then divide by 3). We save this value in a variable and use it for all three channels of the pixel. We need to use the int() function to convert the result to an integer, since we can’t have fractional color values.

Here is code for grayscale(image) that takes in an image and returns a grayscale version.


def grayscale(image):
    """Return a new image containing grayscale values from an image.

    The input should be a PIL image object created using something like
    image = Image.open(filename), where filename is a string.
    """
    # create a copy of the original image so we don't lose the original
    newImage = image.copy()

    # create our 2D structure of pixels from the image so we can work with individual pixels
    pixels = newImage.load()

    # get the bounding box -- this is returned as a tuple and we use some python
    # magic to automatically break the tuple up into four variables
    minX,minY,width,height = image.getbbox()

    # iterate over every pixel in the image
    for y in range(height):
        for x in range(width):
            # get the rgb value of the pixel (as a tuple in the order (red, green, blue))
            rgb = pixels[x,y]

            # set the pixel to a new RGB value 
            # that is the average of the old
            avg = int((rgb[0]+rgb[1]+rgb[2])/3)
            pixels[x,y] = (avg, avg, avg)

    # return the new image
    return newImage

[10 points] Black and White

A different black and white image can be created through thresholding. The idea is to choose a value between 0 and 255 as the threshold, and then any pixel luminance values less than the threshold are considered black, and any pixel luminance values greater than or equal to the threshold are considered white. Implement a function blackwhite(image, threshold) that takes in an image and a threshold value as parameters and returns a black and white version.

[10 points] Sepiatone

Write a function called sepia(image), that takes in an image and returns a sepia version of the image.

Hw08 Sepia


Sepia is a very common photo filter — just about every photo editor has some variant of it. This mimics the effect of an old chemical process that left black and white images yellowish in appearance. We can do this by creating a grayscale image, and then adjusting the red and blue channels to give it a yellowish tint. If we just add some yellow to everything, however, it doesn’t look very good. So we are going to treat the high values, low values and mid values separately.

The process works like this. First calculate the luminance of the pixel as you did above for the grayscale image.

If we just use this luminance value for all three channels, we will get the grayscale image we already created. Instead, create a new red value by multiplying the luminance by the appropriate factor below. Do the same to create a new blue value. The green channel can take the raw luminance value. So, for example, if we have a luminance of 130, you create a new color with (130*1.15, 130, 130*.85) = (149,130,110). If you had a luminance of 50, your color would be (55, 50, 45).

type range of values red weight blue weight
shadow 0-62 1.1 0.9
mids 63-192 1.15 0.85
highlights 193-255 1.08 0.93

Note that since we are increasing the amount of red, it is possible that the red value will exceed 255. If this happens, very strange things can start happening. So, after you have adjusted red and blue, check if red is over 255, and if it is, set it to 255.

[10 points] Better grayscale

The first grayscale image that we produced above looks fine, but we can make it a little better. An old photographer’s trick is to use colored filters when shooting black and white film. Doing this can dramatically change the appearance of the image. For example, we might use a yellow filter when shooting landscapes. This lets through the red and green light (trees, grass, dirt), but blocks out blue (sky and water). This makes foliage glow while making the sky dark and contrasty. Of course with digital photography, we don’t actually need color filters any more, we can just adjust the color channels directly during post-processing.

We are going to do these by allowing the user to specify a weight to be applied to each color channel. Write a function called grayscaleWeighted(image, redWeight, greenWeight, blueWeight)

Instead of averaging the three channel values together, multiply each channel by its respective weight and then sum the result (and convert it to an int). If the weights sum to one, we call this a weighted average. It is still an averaging operation, but it favors one item over another (this is how your grade is calculated so that individual homework assignments are worth less than exams). Note that what we calculated earlier is just a weighted average where the weights all were the same (for three items, the weight would be 1/3).

One interesting set of weights is (0.299, 0.587, 0.114) for red, green, and blue, respectively. This set of weights has been calculated to match the sensitivity of the human eye and is closer to the perceived luminance of a color image.

It is not a requirement that the weights sum to one, but you should keep it in that ball park. Make sure to try a collection of different weights. Often you will find that different weights will dramatically alter your images and there isn’t a single “correct” setting.

Lab08 Grayscale

[10 points] Mirror

This time, rather than altering the colors of the image, we are going to move them around. Write a function called mirror(image) that takes an image and returns another image that has the left side reflected across the center line onto the right side.

Lab08 Mirror


The idea is to visit only the pixels on the left side of the image and to copy the value of the pixel to the appropriate location on the other side of the image (you will need to adjust the end point on the ranges on the for loops so you only visit the pixels on the left side of the image). Here is a diagram showing how the operation should affect the individual pixels:

Lab08 Mirror Diagram

Optional challenge: Four-up

Write a function called fourUp(image) that takes in an image and returns a image that has four 1/4 sized versions of the original in it.

Hw08 Four

There are a number of ways to tackle this problem. Our suggestion is that you add a line before the for loops that creates a new variable (call it something like origPixels) and use the load function to give you access to the pixels in the original image. In your for loop, you will read from the original image and write into the new one.

Start by just writing in the upper left sub image. The image should be one quarter of the size of the original image. This is actually easy to do, just skip every other row and column (i.e., pixel (x,y) in the new image comes from (x2, y2) in the original). When you have it, it should look like this:

Hw08 Quarter

Once you have that working, you should be able to add the three other copies by drawing each pixel you are copying from the original four times. You will need to add offsets to the x and y position to shift the copies to the appropriate quadrant of the new image (think of what location (0,0), the upper left corner, has in each of the sub images). The pattern is very regular, so you should be able to come up with a way of specifying the four locations in terms of x,y, width, and height.


Turning in your work

Before submitting, make sure you have followed all instructions correctly and check the HW7 grading rubric.

Please put all of your functions into a single Python file called username_hw7.py, where username is your Middlebury username.

Be sure to comment your code. The top of the file should include a multiline comment that lists your name, the name of the assignment, and the date, at a minimum. Each function should also include a multiline comment (enclosed in triple-quotes) at the beginning of the body of the function describing what the function does.

Save a copy of each generated image and collect them all in a PDF file called username_hw7.pdf (e.g., here are our sample solutions).

Submit your two files username_hw7.py and username_hw7.pdf using the CS 101 submit script.