Homework 07
Due: 6:00 pm, Tuesday, October 23, 2024
Due Date: Submit your solutions to Gradescope by 6:00 pm, Tuesday, October 23. PLEASE be sure to use proper naming conventions for the files, classes, and functions. We will NOT change anything to run it using our scripts.
Late Policy: Submissions will be accepted up to 24 hours late (6:00 pm on Wednesday) for a 1 point penalty, up to 48 hours late (6:00 pm Thursday) for a 2 point penalty, or up to 72 hours late (6:00 pm Friday) for a 3 point penalty. Submissions past 6:00 pm on Friday will not be accepted.
Unlike the computer lab exercises, this is not a collaborative assignment. See the syllabus and read section “Scholastic Conduct” for information concerning cheating. Always feel free to ask the instructor or the TAs if you are unsure of something. They will be more than glad to answer any questions that you have.
Purpose: The primary purpose of this assignment is to get practice looping through and altering nested list structures.
Instructions: This assignment consists of 5 problems, worth a total of 30 points.
Because your homework file is submitted and tested electronically, the following are very important:
●Submit a file called hw07.py to the Homework 07 submission link on Gradescope by the due date deadline. You don’t need to submit the sample image files.
●Your program should run without errors when we execute it using Python 3.
The following will result in a score reduction equal to a percentage of the total possible points:
●Bad coding style (missing documentation, meaningless variable names, etc.) (up to 30%)
●Breaking problem-specific constraints (up to 40%)
A note on break: While the break keyword can be a useful tool for getting out of loops early, it tends to be misused by intro students to get out of writing a proper loop condition. So for the purposes of this class, the break keyword is considered bad style and will lose you points.
Introduction: Image Manipulation
Every image rendered on a computer can fundamentally be represented as a three-dimensional matrix. An image can be broken down into a two-dimensional array of pixels (short for “picture element”), which appear to be tiny squares of a single color that, when combined, make up the images you see on a computer screen. However, this isn’t quite accurate: each pixel is actually displayed as light produced by three tiny light-emitting diodes: one red, one green, and one blue.
Image credit: https://en.wikipedia.org/wiki/LED_display
For this reason, pixel data is itself stored as a list of three elements, representing the relative strengths of the red, green, and blue diodes needed to produce the required color for the pixel; these are often given as numbers between 0 and 255. Sinc 代 寫Image Manipulation e images are two-dimensional arrays of pixels, and we can represent a pixel as a one-dimensional list of three integers, we can represent an image as a three-dimensional matrix (that is, a list of lists of lists) of integers in Python. In the following problems, you will be manipulating matrices of this format in order to alter image files in various ways. Here is an example of how an image is represented as a 3D matrix:
[[[127, 127, 127], [0, 0, 0]],
[[255, 255, 0], [50, 128, 255]],
[[0, 0, 255], [0, 255, 0]],
[[255, 0, 0], [255, 255, 255]]]
Getting Started
Download the compressed folder labeled hw07files.zip from Canvas, which contains a template file hw07.py, along with bmp_edit.py, and all of the example .bmp files. Extract the files into a folder in a useful location on your computer. Make sure that all the files remain in the same folder.
You’ll only be editing and submitting hw07.py. bmp_edit.py contains functions which convert a bmp image file into a 3D matrix of integers and back - you don’t need to understand or edit these. You also don’t need to submit any of the .bmp images files you generate - only hw07.py is required.
All of the functions you’ll be editing in hw07.py take in a three dimensional matrix that represents an image, as described above, and should return another matrix representing the image with the specified operation performed. It is up to you to decide whether to create a new matrix, or just alter the original to accomplish this.
Constraints
●Do not alter the bmp_edit.py template.
●Do not change the function names or arguments.
●Follow the instructions in the hw07.py template.
●You are permitted to write helper functions outside of the ones provided in the template.
●You are not required to submit any of the .bmp files or bmp_edit.py to Gradescope: only the hw07.py file will be graded.
Documentation
Documentation is provided for you in the hw07.py template 代 寫 Image Manipulation for this assignment, so you don’t need any additional documentation (exception: you do need to fill out the Purpose for the functions in Parts A and E).
Problem A. (4 points) Grayscale
If you haven’t read the introduction, or the information on how to get started, do that first. None of the rest of this will make sense unless you read the background info.
To begin, we’ve given you one function that’s already fully implemented: grayscale. The only thing you have to do in this problem is test it to figure out what it does, and then fill out the “Purpose” section of the documentation.
Copy in the following test case to the bottom of your hw07.py file and run the file:
if __name__ == '__main__':
print(grayscale([[[127, 127, 127], [0, 0, 0]],
[[255, 255, 0], [50, 128, 255]],
[[0, 0, 255], [0, 255, 0]],
[[255, 0, 0], [255, 255, 255]]]))
#[[[127, 127, 127], [0, 0, 0]],
#[[170, 170, 170], [144, 144, 144]],
#[[85, 85, 85], [85, 85, 85]],
#[[85, 85, 85], [255, 255, 255]]]
Note the formatting above is just for clarity: when you copy the code into your file, you won’t actually see each pixel value highlighted in the color it represents, and when you run the tests, each row of the matrix won’t show up on a different line, but the numbers in the output should match the expected output for the test.
Now, let’s see what happens when we run the function on a real file. We’ll use one of the functions imported from the bmp_edit.py file to do this.
At the end of the if __name__ == '__main__': block, add the following lines, and run the file again.
bmp_edit.transform('dice.bmp', grayscale)
bmp_edit.transform('cat.bmp', grayscale)
bmp_edit.transform('connector.bmp', grayscale)
This should take a few seconds to run, but if it works correctly, then new files should be created in the folder containing your hw07.py file called grayscale_dice.bmp, grayscale_cat.bmp, and grayscale_connector.bmp.
If you get a “FileNotFoundError” message, then you’re most likely in the wrong folder in your terminal. Your terminal needs to be pointing to the folder containing your hw07.py file and all the .bmp sample files for this to work. Unlike previous assignments, this applies even if you’re running the program by pressing the “Play” button in VS Code - you still need to use the cd / ls commands in the VS Code terminal to move to the correct location before running the file.
If you can’t figure out this step, ask a TA using office hours or the TA email alias. Once you have it working, we’re actually going to disable the tests before moving on to the next problem - they take a while to complete and we don’t want to wait for them every time we run the file.
Comment out or delete the if __name__ == '__main__': block. Then, fill out the “Purpose” part of the documentation string for the grayscale function based on what the function seems to be doing.
Problem B. (6 points) Rotate Quadrants
Next, you will be fixing the rotate_quadrants function, which is supposed to split the image into four equal sized quadrants, and rotate them clockwise.
The code currently in the template contains a logic error which causes the result to not quite be correct.
Copy the following test case into your hw07.py file, and run it:
if __name__ == '__main__':
print(rotate_quadrants([[[127, 127, 127], [0, 0, 0]],
[[255, 255, 0], [50, 128, 255]],
[[0, 0, 255], [0, 255, 0]],
[[255, 0, 0], [255, 255, 255]]]))
#[[[0, 0, 255], [127, 127, 127]],
#[[255, 0, 0], [255, 255, 0]],
#[[0, 255, 0], [0, 0, 0]],
#[[255, 255, 255], [50, 128, 255]]]
You may notice that the result does not match the expected output - instead you get something like this:
#[[[0, 0, 255], [0, 0, 255]],
#[[255, 0, 0], [255, 0, 0]],
#[[0, 255, 0], [0, 0, 0]],
#[[255, 255, 255], [50, 128, 255]]]
As you can see, the pixels in the lower left quadrant in the original image ([0, 0, 255] and [255, 0, 0]) appear in both the upper left AND upper right quadrants in the output, but the two pixels that started in the upper left quadrant in the original image ([127, 127, 127] and [255, 255, 0]) don’t appear in the output at all.
Fix the logic error in the rotate_quadrants function.
Hints:
●Don’t try these functions on a real bmp file until after you’re sure they work for the smaller examples: it’s much harder to debug mistakes when dealing with such a huge matrix.
●img_matrix[y][x] = img_matrix[y+height//2][x]
represents overwriting a pixel at position (x, y) in the upper left quadrant with the corresponding one from the lower left quadrant. What’s going to happen when we try to move the pixel that was originally in that spot in the upper left quadrant to the corresponding location in the upper right quadrant, with this line?
img_matrix[y][x+width//2] = img_matrix[y][x]
Once you’re sure it’s working for the small example given above, try it on real files:
if __name__ == '__main__':
bmp_edit.transform('dice.bmp', rotate_quadrants)
bmp_edit.transform('cat.bmp', rotate_quadrants)
bmp_edit.transform('connector.bmp', rotate_quadrants)
The resulting image files should appear in the same folder as your hw07.py file, and should look similar to the ones below:
dice.bmp rotate_quadrants_dice.bmp
cat.bmp rotate_quadrants_cat.bmp
connector.bmp rotate_quadrants_connector.bmp
Problem C. (8 points) Lossy Compression
Images can take up a lot of memory: our representation requires 3 bytes per pixel, so if you have a picture that has thousands of rows and columns, it can easily require several MB to store.
One method of dealing with this is lossy compression: sacrificing some of the information depth to save on memory usage. As a very simple example, if we only allowed 0, 100, and 200 as possible values for each pixel, then we could theoretically encode that information with significantly less memory, because we only have to store the most significant digit (0, 1, or 2).
Unfortunately, this won’t actually use any less memory with the image format we’re using (.bmp files), but we can still simulate the results.
Implement the lossy function. In every pixel, for each of the three color components, set the ones’ and tens’ digit to 0.
Hint:
●This will be similar in many ways to the grayscale function we gave you.
●You can use if statements for this, but consider how to do this with integer division instead.
Examples:
if __name__ == '__main__':
print(lossy([[[127, 127, 127], [0, 0, 0]],
[[255, 255, 0], [50, 128, 255]],
[[0, 0, 255], [0, 255, 0]],
[[255, 0, 0], [255, 255, 255]]]))
#[[[100, 100, 100], [0, 0, 0]],
#[[200, 200, 0], [0, 100, 200]],
#[[0, 0, 200], [0, 200, 0]],
#[[200, 0, 0], [200, 200, 200]]]
print(lossy([[[36, 155, 90], [63, 208, 208], [151, 3, 14]],
[[53, 204, 53], [99, 103, 10], [94, 138, 216]]]))
#[[[0, 100, 0], [0, 200, 200], [100, 0, 0]],
#[[0, 200, 0], [0, 100, 0], [0, 100, 200]]]
if __name__ == '__main__':
bmp_edit.transform('dice.bmp', lossy)
bmp_edit.transform('cat.bmp', lossy)
bmp_edit.transform('connector.bmp', lossy)
dice.bmp lossy_dice.bmp
(source: https://en.wikipedia.org/wiki/Dice)
cat.bmp lossy_cat.bmp
connector.bmp lossy_connector.bmp
Problem D. (8 points) Zoom
Implement the zoom function. This takes the upper-left quadrant of the image, and doubles its size to make it fill the entire image.
Assume for this problem that the width and the height of the image are both divisible by 2. For each pixel in the upper left quadrant of the input image, create four pixels in a 2x2 box in the output image, effectively doubling the size of that part of the image. The other three quadrants of the input image can be ignored. See the diagram below for an example.
Hints:
●Similar to Problem B, you’ll run into the issue of potentially overwriting the data in a pixel that we need to read from later.
○Consider making a copy: new_matrix = copy.deepcopy(img_matrix)
○Once we have two copies of the same image, we just need to adapt the rule that one of the images can only be read from (this is the input image), and one can only be written to (this is the output image). In other words, make sure one of the image matrices is never altered, so that you can use that one as a reference for overwriting values in the other one.
●A pixel with coordinates (x, y) in the upper left quadrant of the input image will be copied to the the 2x2 square of pixels at coordinates (2x, 2y), (2x+1, 2y), (2x, 2y+1), and (2x+1, 2y+1) in the output image.
○You could also think of it in reverse: a pixel with coordinates (x, y) in the output image will be copied from the pixel at coordinates (x//2, y//2) in the input image.
Examples:
if __name__ == '__main__':
print(zoom([[[127, 127, 127], [0, 0, 0]],
[[255, 255, 0], [50, 128, 255]],
[[0, 0, 255], [0, 255, 0]],
[[255, 0, 0], [255, 255, 255]]]))
#[[[127, 127, 127], [127, 127, 127]],
#[[127, 127, 127], [127, 127, 127]],
#[[255, 255, 0], [255, 255, 0]],
#[[255, 255, 0], [255, 255, 0]]]
print(zoom(
[[ [255, 0, 0], [255,153,0], [255,255,0],[255,204,51]],
[ [0, 255, 0], [0,255,255],[50,128,255],[255,204,51]],
[ [0, 0, 255], [153,0,255], [255,0,255],[255,204,51]],
[ [0, 0, 0],[255,204,51], [122,0,25] , [122,0,25] ],
[[255,204,51], [122,0,25] , [122,0,25] , [122,0,25] ],
[ [122,0,25] , [122,0,25] , [122,0,25] , [122,0,25] ],
[ [122,0,25] , [122,0,25] , [122,0,25] , [122,0,25] ],
[ [122,0,25] , [122,0,25] , [122,0,25] , [122,0,25] ]]))
#[[ [255, 0, 0], [255, 0, 0], [255,153,0], [255,153,0]],
# [ [255, 0, 0], [255, 0, 0], [255,153,0], [255,153,0]],
# [ [0, 255, 0], [0, 255, 0], [0,255,255], [0,255,255]],
# [ [0, 255, 0], [0, 255, 0], [0,255,255], [0,255,255]],
# [ [0, 0, 255], [0, 0, 255], [153,0,255], [153,0,255]],
# [ [0, 0, 255], [0, 0, 255], [153,0,255], [153,0,255]],
# [ [0, 0, 0], [0, 0, 0],[255,204,51],[255,204,51]],
# [ [0, 0, 0], [0, 0, 0],[255,204,51],[255,204,51]]]
if __name__ == '__main__':
bmp_edit.transform('dice.bmp', zoom)
bmp_edit.transform('cat.bmp', zoom)
bmp_edit.transform('connector.bmp', zoom)
dice.bmp zoom_dice.bmp
cat.bmp zoom_cat.bmp
connector.bmp zoom_connector.bmp
Problem E. (4 points) Your Own Filter
Finally, implement the custom_filter function. You can do whatever you want for this one, so long as you fulfill the following requirements:
●You need to fill in the Purpose section of the Documentation and tell us what your function does.
●The output matrix must be the same dimensions as the input matrix, just like in the other problems.
●The output matrix can’t be identical to the input matrix (you can’t just return img_matrix without doing anything).
●The custom_filter can’t do exactly the same thing as one of the other functions you wrote, or the ones provided for you - no copy-pasting your entire Part C function.
So long as you fulfill the above requirements, you will receive full points on this part, no matter what you choose to do. There’s no extra credit, so put as much or as little effort into this part as you want. Be creative!
Hint:
If you’re not feeling creative and just want to be done, consider the following options, ranked in order from simplest to most complex:
●Set the entire image to a solid blue rectangle.
●Rotate the image 180 degrees.
●Subtract the value in each component (red, green, blue) with the same value in the pixel immediately to the right, then multiply by 8 (rounding up/down to 0 or 255 for values that are out of range if needed). This has the effect of highlighting borders in the image where a pixel sharply differs from its neighbors.
if __name__ == '__main__':
bmp_edit.transform('dice.bmp', custom_filter)
bmp_edit.transform('cat.bmp', custom_filter)
bmp_edit.transform('connector.bmp', custom_filter)