Assignment 6 - Image Processing

Due: Thursday, October 30, 2025, 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 should submit your assignment as a imageProcessing.py file on Gradescope.


Getting started:

You will do most 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.

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.

Here are some other cute ones: Cheddar, Lulu, Hobbes, and Marshmallow.

Copy any .gif image files you want to use into a folder named images that lives in the same folder as imageProcessing.py.


Goals

The primary goal for this assignment is to give you practice working with nested for loops. You will additionally get practice working with images and colors.


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.

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 if it bothers you, you are encouraged to take some time to clean up your code (see below for suggestions).


Part 1: Inverting an image

# You should be fully equipped to complete this part after Lesson 16 (Wednesday Oct. 22).

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

Additionally, you should implement the getInverseColor function, should choose a color that is the inverse of that at pixel (x,y) in the parameter image:

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

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 16 (Wednesday Oct. 22).

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

Part 3: Blurring an image

# You should be fully equipped to complete this part after Lesson 16 (Wednesday Oct. 22).

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

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

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:


NOT REQUIRED: Code refactoring

This part is not something you should submit, nor will it be graded, but these are the natural next steps to make this program a better version of itself.

To start this part, make a copy of imageProcessing.py named imageProcessingRefactored.py.

The get<Something>Color functions serve as examples of polymorphism: all take in an image and the x and y coordinates of a pixel and return a color. Replace your create<Something>Image functions with a single function that takes a getColorFunc function as a new parameter and returns the new image.

Make sure to always use the original image when calling the getColorFunc function!

Add this to your program, remove the existing create<Something>Image functions, and then “refactor” your program to call the createImage function instead:

def createImage(origImage, getColorFunc):
    """
    Creates a copy of the original image, and recolors the copy using
    getColorFunc for each pixel in the original image.

    origImage: an Image object to copy
    getColorFunc: the function to call for each pixel

    returns: the new image
    """
    return None # TODO: implement me!

For each call to createImage, your code needs to provide the name of a get<Something>Color function, rather than call the function. In createImage, you should invoke that function by using getColorFunc(). For example, a call to createImage might look like this:

    grayscaleImage = createImage(image, getGrayscaleColor)

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 contain 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 for getInverseColor and 10 pts for 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 Gradescope.