Instagram claims that it is site for sharing photos online with friends. But it became popular for its easy to use photo editing tools and image filters In this assignment, you will learn how to make your own image filters. While they may not be as fancy as the ones provided by Instagram, this assignment will still teach the basic principles to help you with your own start-up someday.
As we have seen in class, an image is just a 2-dimensional list of pixels (which are themselves RGB objects). So must of the functions/methods that you write in this assignment will involve nested for-loops that read a pixel at a certain position and modify it. You will need to use what you have learned about multi-dimensional lists to help you with this assignment.
One major complication is that graphics cards prefer to represent images as a 1-dimensional list in a flattened presentation instead. It is much easier for hardware to process an 1-dimensional list than a 2-dimensional one. However, flattened presentation (which we explain below) can be really confusing to beginners. Therefore, another part of this assignment is learning to use classes to abstract a list of pixels, and present it in an easy-to-use package.
Finally, this assignment will introduce you to a Python package. This application is a much more complex GUI than the one that you used in Assignment 3. While you are still working on a relatively small section of the code, there are a lot of files that go into making this application work. Packages are how Python groups together a large number of modules into a single application.
Important: This is longer than previous assignments. To reduce stress, we highly recommend that you follow the recommended micro-deadlines.
November 5: There were some serious typos in the specifications for class Image
. These are now fixed in the source code and the instructions.
Authors: W. White, D. Gries, and D. Kozen
Learning Objectives
This assignment is designed to give you practice with the following skills:
- How to implement a class from its interface.
- How to enforce class invariants.
- How to use a classes to provide abstractions.
- How to write code for both 1-dimensional and 2-dimensional lists.
- How to manipulate images at the pixel level.
Table of Contents
Academic Integrity and Collaboration
This assignment is similar to one that we have given occasionally in the past, though it is heavily modified from previous years. Once again we still ask that you do not consult or seek out earlier versions of this assignment. Consulting any prior solution is a violation of CS1110’s academic integrity policy.
In this assignment, it is highly unlikely that your code for this assignment will look exactly like someone else’s. We will be using Moss to check for instances of plagiarism. We also ask that you do not enable violations of academic policy. Do not post your code to Pastebin, GitHub, or any other publicly accessible site.
Collaboration Policy
You may do this assignment with one other person. If you are going to work together, form your group on CMS as soon as possible. If you do this assignment with another person, you must work together. It is against the rules for one person to do some programming on this assignment without the other person sitting nearby and helping.
With the exception of your CMS-registered partner, we ask that you do not look at anyone else’s code or show your code to anyone else (except a CS1110 staff member) in any form whatsoever. This includes posting your code on Piazza to ask for help. It is okay to post error messages on Piazza, but not code. If we need to see your code, we will ask for it.
Assignment Help
This assignment is a significant jump in difficulty from the previous programming assignment. Waiting to do this assignment until the last minute (or even the last week) is guaranteed to fail. We suggest that you start this assignment early, and that you go to office hours page on the first sign of difficulty.
Assignment Overview
This assignment requires you to implement several parts before you have the whole application working. The key to finishing is to pace yourself, and make use of both the unit tests and the visualizer that we provide.
Assignment Source Code
To work on this assignment, you will need to download three files.
File | Description |
---|---|
imager.zip | The application package, with all the source code |
samples.zip | Several sample images to test in the application |
outputs.zip | The result of applying the filters to the sample images |
You should download the zip archive imager.zip
from the link above.
Unzip it and put the contents in a new directory. This time, you will
see that this folder contains a lot of files. You do not not need to
understand most of these files. The are similar to a3app.py
in that
they provide the GUI interface for the application.
You only need to pay attention to the files that start with a6
. There
are four of these. Two are completed and two are only stubs, waiting
for you to complete them.
File | Description |
---|---|
a6image.py | The Image class, to be completed in Task 1 |
a6editor.py | The Editor class, which is completed already |
a6filter.py | The Filter class, to be completed in Task 2 |
a6test.py | The test script for the assignment, which is completed already |
You should skim all of these files before continuing with the assignment instructions.
Suggested Micro-Deadlines
This assignment has a problem: it is due at the end of in-person classes, right before
semi-finals. There is not too much we can do about this (other than making it due earlier).
Furthermore, everything in this assignment will be covered on the second prelim/semi-final.
So you really want to have this assignment completed beforehand, as it is one of the
best ways to study.
With that said, we understand that everyone is going to be piling onto you at this time. That is why, at the end of each part of the assignment, we have a suggested completion date. While this is not enforced, we recommend that you try to hit these deadlines. If you cannot make these deadlines, it might be a sign that you are having difficulty and need some extra help. In that case, you should go to office hours page as soon as possible.
The Imager Application
Because there are so many files involved, this application is handled a little differently
from previous assignments. To use this application, keep all of the files inside of the
folder imager
. Do not rename this folder. To run the program, change the
directory in your command shell to just outside of the folder imager
and type
In this case, Python will run the entire folder. What this really means is that
it runs the script in __main__.py
. This script imports each of the other
modules in this folder to create a complex application.
Right now, this application will not do anything. However, once you
complete the Image
class, it will display two images
of your instructor, like this:
As you work with this application, the left image will not change; it is the original image. The right image will change as you click buttons. The actions for the buttons Invert and Rotate.., are already implemented. Click on them to see what they do.
You will notice that this takes a bit of time (your instructor’s computer takes 2-3 seconds for most operations). The default image is 512x512. This is over 250 thousand pixels. The larger the image, the slower it will be. With that said, if you ever find this taking more than 30 seconds, your program is likely stuck and has a bug in it.
The effects of the buttons are cumulative. You can undo the last effect applied with Image.. Undo. To remove all of the effects, choose Image.. Clear. This will revert the right image to its original state.
You can load a new image at any time using the Image.. Load button. Alternatively, you can start up with a new image by typing
where myimage.png
is your image file. The program can handle PNG, JPEG, and GIF
(not animated) files. You also can save the image on the right at any time by
choosing Image.. Save. You can only save as a PNG file.
The remaining buttons of the application are not implemented. Reflect.. Horizontal works but the vertical choice does not. In Part 2 of the assignment, you will write the code to make them work.
If you are curious about how this application works, most of the code is in
interface.py
and interface.kv
. The file interface.kv
arranges the buttons
and colors them. The module interface.py
contains Python code that tells what the
buttons do. However, the code in this module is quite advanced and we do not
expect you to understand any of it.
The Integrated Test Script
As with Turtles, debugging everything visually can be tricky. That is why we have provided you with a (partial) test script to help you with this assignment. This test script is integrated into the Imager application. To run it, type
The application will run test cases (provided in a6test.py
) on the classes
Image
and Filter
in that order. This is incredibly useful, since you cannot
even use the Imager app until you finish the Image
class.
These test cases are designed so that you should be able to test your code in the order
that you implement it. Howevever, if you want to “skip ahead” on a feature, you are
allowed to edit a6test.py
to remove a test. Those tests are simply there
for your convenience.
This test script is fairly long, but if you learn to read what this script is doing, you will understand exactly what is going on in this assignment and it will be easier to understand what is going wrong when there is a bug. However, one drawback of this script is that (unlike a grading program), it does not provide a lot of detailed feedback. You are encouraged to sit down with a staff member to talk about this test script in order to understand it better.
As with the Turtles assignment, this test script is not complete. It does not have
full coverage of all the major cases, and it may miss some bugs in your code. It is
just enough to ensure that the GUI application is working correctly. You may lose points
during grading even if you pass all the tests in this file (our grading program has
a lot more tests). Therefore, you may want to add additional tests as you debug.
With that said, we do not want you to submit the file a6test.py
when you are done,
even if you made modifications to the file.
Assignment Instructions
There are so many parts to the Imager application that this assignment can feel very overwhelming. But in these instructions we take you through everything step-by-step. As long as you pay close attention to the specifications, you should be able to complete everything. This assignment may take longer than the others, but it is well within your ability.
Task 0: Pixel Representation
You do not ever need to worry about writing code to load an image from a file.
There are other modules in imager
that handle that step for you. Those modules
use the PIL module to extract
pixel data from a file. The functions in this module return the image as a
flattened list of pixels.
To understand what we mean by this, let’s talk about pixels first. A pixel is a single RGB (red-green-blue) value that instructs you computer monitor how to light up that portion of the screen. Each RGB component is given by a number in the range 0 to 255. Black is represented by (0, 0, 0), red by (255, 0, 0), green by (0, 255, 0), blue by (0, 0, 255), and white by (255, 255, 255).
In previous assignments, we stored these pixels as an RGB
object defined in
the introcs
module. These were mutable objects where you could change
each of the color values, and these objects would automatically enforce the 0..255
invariant. However, the pixels in this assignment will be 3-element tuples
of integers. That is because they are faster to process, and Kivy prefers this format.
Because image processing is slow enough already, we have elected to stick with this
format. In addition, this means that you get some experience checking and enforcing
that the pixels are in the correct format.
So if that is what we mean by a pixel, what is a “flattened list of pixels”? We generally think of an image as a rectangular table of pixels, where each pixel has a row and column (indicating its position on the screen). For example, a 3x4 pixel art image would look something like the illustration below. Note that we generally refer to the pixel in the top left corner as the “first” pixel.
However, graphics cards really like images as a one-dimensional list. One-dimensional lists are a lot faster to process and are more natural for custom hardware. So a graphics card will flatten this image into the following one-dimensional list.
If you look at this picture carefully, you will notice that is it is very similar to row-major order introduced in class. Suppose we represented the 3x4 image above as follows:
E00 E01 E02 E03
E10 E11 E12 E13
E20 E21 E22 E23
The value Eij
here represents the pixel at row i
and column j
. If were were
to represent this image as a two-dimensional list in Python, we would write.
Flattened representation just removes those inner brackets, so that you are left with the one-dimensional list.
This is the format that the Image
class will use to store the image. If you
do not understand this, you should talk to a staff member
before continuing.
Precondition Enforcement
Throughout this assignment, you will be asked to enforce preconditions. A common
precondition that will come up over and over again is that a value is a pixel,
or a value is a pixel list. Inside of the file a6image.py
are two helper functions
to help you enforce these preconditions: _is_pixel
and _is_pixel_list
. The
first has been completed for you. The second is unfinished.
Before you do anything else, complete the function _is_pixel_list
. Despite the
fact that this is a hidden function, we do test it in a6test.py
. So you
should run the test script to verify that your
implementation is correct.
Recommended Deadline
This is not a hard function, and it is very similar to some of the nested-loop functions you have seen in class. But you did need to read all of these instructions to get this far. So we recommend that you finish this part by Wednesday, November 4. Finishing this part of the assignment will demonstrate that you understand how pixels work and allow you to get started on the assignment.
Task 1. The Image
Class
For some applications, flattened representation is just fine. For example, if you want to convert an image to greyscale, you do not need to known exactly where each pixel is inside of the file. You just modify each pixel individually. However, other effects like rotating and reflecting require that you know the position of each pixel. In those cases you would rather have a two-dimensional list.
The Image
class has attributes and methods that allow you to treat the image
either as a flattened one-dimensional list or as a two-dimensional list,
depending on you application. This is what we mean by an abstraction.
While the data is not stored in a two-dimensional list, methods like
getPixel(row,col)
allows you to pretend that it is.
The file a6image.py
contains the class definition for Image
. This class is
fully specified. It has a class specification with the class invariants.
It also has specifications for each of the methods. All you have to do is to
write the code to satisfy these specifications.
As you work, you should run the test cases to verify that your code is correct. To get the most use out of the testing program, we recommend that you implement the methods in the same order that we test them.
The Initializer, Getter and Setter Methods
November 5: There are some fixes to the invariants here.
Namely _data
cannot be empty and neither _width
nor _height
can be zero.
This has been updated in the source code.
To do anything at all, you have to be able to create an Image
object and
access the attributes. This means that you have to complete the initializer
and the getters and setters for the three attributes: data, width and height.
If you read the specifications, you will see that these are all self-explanatory.
Note that these attributes are hidden, so the class invariant is given by
(hidden) single line comments
according to our specification style.
The only challenge here is the width and height. Note that there is an extra invariant that the following must be true at all times:
width*height == # of pixels
You must ensure this invariant in both the initialiers and the setters. In addition, we expect you to enforce all preconditions with asserts.
Recommended Deadline
Except for the setters for width
and height
(which have the unusual invariant),
this part is no harder than the Length
class in
Lab 17. So you should be
able to do this part quickly. We want you to get in the habit on
working on this assignment a little bit every day. That will make this assignment
easier and less stressful. That is why we recommend that you finish this part by
Thursday, November 5.
The One-Dimensional Operators
The getter getData
already returns the image data as a flattened list of pixels.
So you might think we do not need to do anything more here. However, notice that
getData
returns a copy of the pixel list. So it is not useful if you want to
modify the image. Instead, the class Image
has methods to allow modification
of the image, while still enforcing the class invariant.
The methods for one-dimensional access are all operators, which are introduced
in class on Tuesday, November 10.
They are special methods that begin and end with double-underscores (like __init__
or __str__
). But you can still do this part of the assignment without understanding
that lesson – just read the specifications. The methods to implement are __len__
,
__getitem__
and __setitem__
.
If you are curious what these methods do, __len__
is a helper method for the
len
function. So the following two lines of code are identical.
The __getitem__
method allows you to use square brackets to access a pixel in an
Image
object (just one pixel, not a slice). Once you implement it, the following
two lines of code are identical.
Finally, the __setitem__
method allow you to use brackets on an Image
object to
replace a pixel. Once you implement it, the following two lines of code are identical.
To see more, look at the tests in a6test.py
.
The code for all of these methods is incredibly simple. For each method you just have
one line to access or update the _data
attribute. So why have these methods? To
enforce the preconditions, of course. A simple list does not care whether its contents
are valid pixels. But this is required by the class invariant and you must enforce
this to prevent the user from adding invalid data to the list (do not assert anything
about _data
; just assert the preconditions).
Recommended Deadline
Again, this is another short bit of code. You should be able to finish this and the next part (Two-Dimensional Methods) by Friday, November 6.
The Two-Dimensional Methods
The getPixel
and setPixel
methods present the image as a two-dimensional list.
This is a little trickier. You have to figure out how to compute the flat position
from the row and column. Here is a hint: it is a formula that requires the row,
the column, and the width. You do not need anything else. Look at the illustrations
in our discussion of flattened representation
to see if you can figure out the formula.
Figuring out the conversion formula is the only hard part of this exercise. Otherwise it is the same as for the one-dimensional operators. Make sure to enforce all of the preconditions.
Recommended Deadline
Because this is very similar to the one-dimensional operators, you should try to finish this by the same day: Friday, November 6. This pace will put you in good shape for the more complicated functions in Task 2.
The __str__
Method
This method is arguably the hardest one in the entire Image
class. We want you to
create a string that represents the pixels in two-dimensional row-major order. As
a hint, this is a classic for-loop problem. You will need an accumulator to store
the string, and you will build it up via concatenation. You will also want to use
previous methods that you wrote as helpers.
You might think that all you need to do is to accumulate the pixels into a two-dimensional list and convert that list to a string. This would be correct except for the newlines between rows. You need to be a little more clever here. Again, use the the test script to verify that your code is correct.
Recommended Deadline
Because this method is harder than the previous ones, you may need to spend a full day on this method and this method alone. However, ideally you should finish this method by Saturday, November 7.
If you are get stuck on this method, you can skip it and come back to it later. The primary purpose of this method is to help you with debugging (so you can examine the contents of an image) in Task 2. But it is not required for any other part of the assignment, so you can skip ahead if you are having difficulty.
The Remaining Methods
The remaining two methods are swapPixel
and copy
. These are very simple once you
have implemented everything else. Finish them, test them, and you are done with the
Image
class. At this point you should be able to
launch the imager application and start to play with it.
Recommended Deadline
This is the last bit of the Image
class. No matter how long it took you to do
the previous methods, we highly recommend that you have this class finished by
Sunday, November 8.
That will give you a week to finish the image processing features, which are more
sophisticated.
Task 2. The Filter
Class
The module a6filter.py
contains the Filter
class. You will notice that it is
a subclass of the Editor
class in a6editor.py
. The Editor
class is
complete; you do not have to do anything with this class. It implements the
Undo functionality in the imager
application. This class implements an
edit history and the getter getCurrent
accesses the most recent update of the image.
You do not need to understand the Editor
class at all, but you should read its
specification. Since Filter
is a subclass, it will need to access the inherited
methods from Editor
. In particular, you will notice that none of the methods
in Filter
take an image as an input. Instead, those methods are to work on the
current image, which they access with the method getCurrent
.
To make it easier to follow all this, we have provided you with several example
methods to study. You will notice that some filters – like invert
– modify
the image with the one-dimensional operators. Others – like transpose
and
reflectHori
– modify the image with the two-dimensional operators. Use this
code as a guide for implementing the unfinished methods.
While working on these methods, you may find that you want to introduce new helper
methods. For example,jail
already has a _drawHBar
helper provided. You may find
that you want a _drawVBar
method as well. This is fine and
is actually expected. However, you must write a complete and thorough specification
of any helper method you introduce. It is best to write the specification before you
write the method body, which is standard practice in this course. It is a
severe error not to have a specification, and points will be deducted for missing
or inappropriate specifications.
We have provided you with several test cases for these filters. But the output of these test cases are limited and not always useful. A better way to test is just to load the sample images and try them out. We have provided you with the correct outputs for each filter applied to each sample image.
Method reflectVert
This method should reflect the image about a horizontal line through the middle of the
image. Look at the method reflectHori
for inspiration, since it does something similar.
This method should be relatively straightforward. It is primarily intended as a warm-up
to give you confidence.
Recommended Deadline
Because it is just a warm-up, you should be able to complete this and the next method in one day. Assuming that you are keeping up with the recommended deadlines, this means you should complete this part by Monday, November 9.
Method monochromify
In this method, you will change the image from color to either grayscale or sepia tone.
The choice depends on the value of the parameter sepia
. To implement this method, you
should first calculate the overall brightness of each pixel using a combination of the
original red, green, and blue values. The brightness is defined by:
For grayscale, you should set each of the three color components (red, green, and blue)
to the same value, int(brightness)
.
Sepia was a process used to increase the longevity of photographic prints. To
simulate a sepia-toned photograph, darken the green channel to
int(0.6 * brightness)
and blue channel to int(0.4 * brightness)
,
producing a reddish-brown tone. As a handy quick test, white pixels stay white for
grayscale, and black pixels stay black for both grayscale and sepia tone.
To implement this method, you should get the color value for each pixel, recompute a new
color value, and set the pixel to that color. Look at the method invert
to see how this is done.
Recommended Deadline
If you can figure out how to properly use the brightness
value, this is another
short method. We recommend that you complete this part by
Tuesday, November 10.
That way you will have more time for the harder methods.
Method jail
Always a crowd favorite, the jail
method draws a a red boundary and
vertical bars on the image. You can see the result in the picture below. The
specification is very clear about how many bars to create and how to space them.
Follow this specification clearly when implementing the function.
We have given you helper method _drawHBar
to draw a horizontal bar (note that
we have hidden it; helper functions do not need to be visible to other modules
or classes). In the same way, you should implement a helper method _drawVBar
to draw a vertical bar. Do not forget to include its specification in your code.
This method is one where you have to be very careful with round-off error, to make
sure that the bars are evenly spaced. You need to be aware of your types at all times.
The number of bars should be an integer, not a float (you cannot have part of a bar).
However, the distance between bars should be a float. That means your column position
of each bar will be a float. Wait to turn this column position into an
int
(by rounding and casting) until
you are ready to draw the bar.
When you are finished with this method, open a picture and click the buttons Jail, Transpose, Jail, and Transpose again (in that order) for a nice effect.
Recommended Deadline
This method is a little more complicated than the previous filters, but it is still not that bad. The hardest part of this method is making sure that you are handling the round-off error correctly. We recommend that you finish this method by Wednesday, November 11.
Method vignette
Camera lenses from the early days of photography often blocked some of the
light focused at the edges of a photograph, producing a darkening toward the corners.
This effect is known as vignetting
, a distinctive feature of old photographs.
You can simulate this effect using a simple formula. Pixel values in red, green, and
blue are separately multiplied by the value
1 - d2/h2
where d
is the distance from the pixel to the center of the image and
h
is the distance from the center to any one of the corners. The effect
is shown below.
Like monochromification, this requires unpacking each pixel, modifying the RGB values, and repacking them (making sure that the values are ints when you do so). However, for this operation you will also need to know the row and column of the pixel you are processing, so that you can compute its distance from the center of the image.
For this reason, we highly
recommend that you use the method getPixel
and setPixel
in the class Image
. These methods treat the image as a
two-dimensional list. Do not use the one-dimensional operators. That was
fine for invert
and monochromify
, but that was because the row and column
did not matter in those methods.
Recommended Deadline
This is a much harder method than the previous methods, and it may take you more than a day to complete it. However, the final method is also hard so you want to give yourself enough time to complete that as well. Therefore, we recommend that you finish this method by Friday, November 13. This will give you enough time to complete the assignment.
Method pixellate
This last method is quite challenging. We do not expect everyone in the class to get it correct. Indeed, this is an expectation that we have going into the final assignment, so you will get a preview of that here. Be assured, however, that if you can get everything else on this assignment except for this problem, you will still get at least a B+/A- for the whole assignment.
One of the reasons that pixellate
is more difficult is because we will be applying a
special rule to this method: the no code rule. You may not show your code to a
a course staff member to get explicit help on the method. We will still look at
error messages. And we are willing to draw you pictures to help you understand what
the method should do. But the level of help that you can receive on this problem is
not much more than what you can get on Piazza.
As for the method itself, pixellation simulates dropping the resolution of an image. You do not actually change the resolution (that is a completely different challenge). However, you replace the image with large blocks that look like giant pixels.
To construct one of these blocks, you start with a pixel position (row
,col
).
You gather all of the pixels step
many positions to the right and below and average
the colors. Averaging is exactly what it sounds like. You sum up all the red values
and divide them by the number of pixels. You do the same for the green and blue values.
When you are done averaging, you assign this average to every pixel in the block. That
is every pixel starting at (row
,col
) and within step
positions to the right or
down gets this same pixel color. This result is illustrated below.
When you are near the bottom of the image, you might not have step
pixels
to the right or below (and this is what makes this question hard). In that case,
you should go the edge of the image and stop. We highly recommend that you write
this averaging step as helper function. It will greatly simplify your code in
pixellate
.
One thing you do need to watch out for is how you construct your loops in
pixellate
. If you are not careful, the blocks will overlap each other,
messing up the pixellation effect. Think very carefully about what you want
to loop over.
Finishing Touches
Before you submit this assignment, you should be sure that everything is working and polished. Unlike the first assignment, you only get one submission for this assignment. If you make a mistake, you will not get an opportunity to correct it. With that said, you may submit multiple times before the due date. We will grade the most recent version submitted.
Once you have everything working you should go back and make sure that your program meets the class coding conventions. In particular, you should check that the following are all true:
- You have indented with spaces, not tabs (Atom Editor handles this automatically).
- Functions are each separated by two blank lines.
- Methods are each separated by one blank line.
- Lines are short enough (~80 characters) that horizontal scrolling is not necessary.
- Docstrings are only used for specifications, not general comments.
- Specifications for any new methods are complete and are docstrings.
- Specifications are immediately after the method header and indented.
- Your name(s) and netid(s) are in the comments at the top of the modules.
You will submit only two files for this assignment: a6image.py
and a6filter.py
.
Upload these files to CMS by the due date:
Sunday, November 15. We do not need any
other files. In particular, we do not want the file a6test.py
.
Completing the Survey
As always, this assignment comes with a survey. More than any other assignment
this year, we are particularly interested in the results of this survey.
Please keep track of the hours that you spent on this assignment. Older versions
of this assignment averaged 16-18 hours, which is why we stopped offering it.
We have scaled back the difficulty this year. Previously, Editor
was part of
the assignment, and we had a third class that involved storing secret messages in
the images. We are very interested in how long this assignment takes.
Please try to complete the survey within a day of turning in this assignment. Remember that participation in surveys is 1% of your final grade.