Assignment 5 - Image Processing

Due: Friday, February 2, 2024, at 10pm

You may work alone or with a partner, but you must type up the code yourself. You may also discuss the assignment at a high level with other students. You should list any student with whom you discussed each part, and the manner of discussion (high-level, partner, etc.) in a comment at the top of each file. You should only have one partner for an entire assignment.

You will do all of your work in a single file, imageProcessing.py. You should download this “skeleton” version, and save it as imageProcessing.py in a folder that also has graphics.py.

You should submit your assignment as a imageProcessing.py file on Moodle.

Parts of this assignment:

Comments and collaboration

As with all assignments in this course, for each file in this assignment, you are expected to provide top-level comments (lines that start with # at the top of the file) with your name and a collaboration statement. For this assignment, you have multiple programs; each needs a similar prelude.

You need a collaboration statement, even if just to say that you worked alone.

Note on style:

The following style guidelines are expected moving forward, and will typically constitute 5-10 points of each assignment (out of 100 points).

  • Variable names should be clear and easy to understand, should not start with a capital letter, and should only be a single letter when appropriate (usually for i, j, and k as indices, potentially for x and y as coordinates, and maybe p as a point, c for a circle, r for a rectangle, etc.).
  • It’s good to use empty lines to break code into logical chunks.
  • Comments should be used for anything complex, and typically for chunks of 3-5 lines of code, but not every line.
  • Don’t leave extra print statements in the code, even if you left them commented out.
  • Make sure not to have code that computes the right answer by doing extra work (e.g., leaving a computation in a for loop when it could have occurred after the for loop, only once).
  • Avoid having tons of lines of code immediately after another that could have been in a loop.

Note: The example triangle-drawing program on page 108 of the textbook demonstrates a great use of empty lines and comments, and has very clear variable names. It is a good model to follow for style.

Note: For this assignment, it is okay for some of the functions you implement to be incredibly similar to each other – this code duplication is deliberate, and we will talk in class about how to reduce it greatly.

Here is an example image after you have completed all parts of this assignment. Make sure to try it out on an image of your own! (Note that it seems that the image has to be a .gif or maybe a .png file unless you install additional libraries.) Also, it’s a good idea to use fairly small images for this assignment, so you can view all six versions of the image at once. An image no larger than 400x300 would be a good choice – you can use many freely available programs to resize an image.

<image: different image processing outputs>

The original image is here.

Part 1: Inverting an image

# You should be fully equipped to complete this part after Lesson 11 (Friday Jan. 26).

Before you get started, read through the code. Anything with a # TODO is something you’ll need to complete. For this first part, you will implement two functions.

def getInverseColor(image, x, y):
    """
    Converts the pixel to its inverse by taking the complement of
    each color channel.

    returns: the result of color_rgb
    """
    # TODO: Part 1
    return None # replace with your code

This function should choose a color that is the inverse of that at pixel (x,y) in the parameter image.

Additionally, you should implement the createInverseImage function:

def createInverseImage(origImage):
    """
    Creates a copy of the original image, and inverts its colors.

    returns: the inverse image
    """
    # TODO: Part 1
    return Image(Point(0,0), 1, 1) # replace with your code

Before you dive into the code for this part, think carefully about how it differs from converting an image to grayscale, or drawing an image in which every pixel is blue.

Part 2: Changing to sepia tone

# You should be fully equipped to complete this part after Lesson 11 (Friday Jan. 26).

For Part 2, you should implement functions to construct a sepia-toned image. This is very similar to the grayscale image, but the formula for a given pixel is different.

def getSepiaColor(image, x, y):
    """
    Converts the pixel to sepia tone using the following formula:
       newR = 0.393*r + 0.769*g + 0.189*b
       newG = 0.349*r + 0.686*g + 0.168*b
       newB = 0.272*r + 0.534*g + 0.131*b

    Note that if any value is over 255, it must be capped at 255.

    returns: the result of color_rgb
    """
    # TODO: Part 2
    return None # replace with your code
def createSepiaImage(origImage):
    """
    Creates a copy of the original image, and colors it sepia toned.

    returns: the sepia image
    """
    # TODO: Part 2
    return Image(Point(0,0), 1, 1) # replace with your code

Part 3: Blurring an image

# You should be fully equipped to complete Part 3 after Lesson 11 (Friday Jan. 26).

For this final part, you will implement functions to blur an image. In Parts (a) and (b) you will implement two pairs of functions that blur at different granularities.

Part a: A little blurry

Everything we’ve done so far to manipulate images has taken the color at a given pixel and used that to choose the color at the same pixel in the new image. To blur an image, we need to consider more pixels in the original.

The image below on the left represents what you did in Parts 1 and 2. A given pixel in the new image was based on the color from just a single pixel in the original. The image on the right represents the kernel used to blur the image. For a given pixel (shaded in blue), you should take the average of each of the red, green, and blue color channels for that pixel and its eight neighbors from the original image. The 1 in each cell represents that it is not a weighted average, but rather that you treat each pixel in the average equally.

<image: image processing kernels>

What makes this challenging is handling the edge cases (literally). The image below shows two edge/corner cases for a kernel that is 3x3 pixels.

<image: edge cases in a 3x3 kernel>

You should avoid dealing with the edge cases; instead, simply use black (all values 0) for the one-pixel-wide border of the image.

For part (a), you should implement these two functions:

def getBlurryColor(image, x, y):
    """
    Chooses a color to blur an image by taking the average
    across each color channel (R/G/B) of the 3x3 square of
    pixels centered at (x,y).

    Note that if (x,y) is on a border, it should just be black.

    returns: the result of color_rgb
    """
    # TODO: Part 3a
    return None # replace with your code
def createBlurryImage(origImage):
    """
    Creates a copy of the original image, and blurs it.

    returns: the blurry image
    """
    # TODO: Part 3a
    return Image(Point(0,0), 1, 1) # replace with your code

Part b: A bit blurrier

For Part (b), you should create an even blurrier image by averaging over more pixels. Rather than a 3x3 kernel, you should use a 5x5 kernel to implement these two functions:

def getBlurrierColor(image, x, y):
    """
    Chooses a color to blur an image by taking the average
    across each color channel (R/G/B) of the 5x5 square of
    pixels centered at (x,y).
    
    Note that if (x,y) is on a border or one pixel away, it
    should just be black.

    returns: the result of color_rgb
    """
    # TODO: Part 3b
    return None # replace with your code
def createBlurrierImage(origImage):
    """
    Creates a copy of the original image, and blurs it a lot.

    returns: the blurrier image
    """
    # TODO: Part 3b
    return Image(Point(0,0), 1, 1) # replace with your code

Note that in this case, there are even more corner/edge cases to consider.

<image: edge cases in a 5x5 kernel>

You should avoid dealing with the edge cases; instead, simply use black (all values 0) for the two-pixel-wide border of the image.

Testing your code

The squash image isn’t the greatest to test your code. Here are a few you can try, too:

Reflection

# You should be equipped to complete this part after finishing your assignment.

Were there any particular issues or challenges you dealt with in completing this assignment? How long did you spend on this assignment? Write a brief discussion (a sentence or two is fine) in comments at the bottom of your imageProcessing.py file.

Grading

This assignment will be graded out of 100 points, as follows:

  • 5 points - submit a valid imageProcessing.py file with the right name

  • 5 points - imageProcessing.py file contains top-level comments with file name, purpose, author names, and collaboration statement

  • 10 points - code style enables readable programs

  • 20 points - Part 1 (10 pts each for getInverseColor and createInverseImage)

  • 15 points - Part 2 (10 pts for getSepiaColor and 5 pts for createSepiaImage)

  • 25 points - Part 3a (20 pts for getBlurryColor and 5 pts createBlurryImage)

  • 15 points - Part 3b (10 pts for getBlurrierColor and 5 pts for createBlurrierImage)

  • 5 points - reflection in the comments at the bottom of the imageProcessing.py file

What you should submit

You should submit a single imageProcessing.py file on Moodle.