Assignment 6: Images
Due to CMS by Saturday, April 13th at 11:59 pm
These instructions last updated on April 08 2013 18:19.
Added authorship info and section numbers.
Authors: D. Gries, L. Lee, S. Marschner, W. White.
Digital imaging is a ubiquitous tool in modern life: we snap photos with our phones, share them with
friends and family, crop them, edit them, post them to online galleries, and it's all done with
two-dimensional arrays of triples of numbers, giving the red, green, and blue values for each pixel
that makes up the image. Lots of photo editing applications provide tools to modify images to your
liking, and they all work by modifying these arrays of pixel values.
You
can represent an image as a 2-dimensional list of
color pixels. To avoid the overhead created by lists of lists in Python, however,
it is common to
represent an image as a 1-dimensional list with the pixels all listed together, row
by row—known as row-major order,
as described below. Calculating indices into a row-major array can make code hard to read, so
this assignment, like most code that deals with images, encapsulates
the image's pixel list inside a class (in our case ImageArray ) that provides
an abstraction to let us treat the list of pixels as a two-dimensional array—or as
a 1D array depending on our preference. So in addition to learning
about images, you will also see encapsulation in action.
Reading the Instructions
These instructions are fairly long, and
you should spend an evening simply reading these instructions while looking
over the existing classes. Even though you don't need to understand every detail of
the ImageArray class, or especially of the user interface, you should be
able to see how the program fits together: what calls what, which data goes where.
It's important to understand the overall plan before you start writing your code.
Learning Objectives
This assignment has several important objectives.
- It gives you practice with writing loops and using invariants.
- It gives you practice with writing loops to process one- and two-dimensional sequences.
- It gives you practice with using helper methods and constants to structure your code properly.
- It gives you practice with using encapsulation to make difficult programming concepts easier.
- It introduces you to the concept of a tuple, which is essentially an immutable list.
- It gives you practice with manipulating images at the pixel level.
Table of Contents
Before You Get Started
Read this entire document carefully. Budget your time wisely. In particular, you should
get started on this assignment immediately. You have just over a week to work on
this assignment, and there is no room to grant (further) extensions, with the second prelim coming
on its heels.
There are six methods in total. We recommend that you work on at least one method a day.
Academic Integrity
We have used various versions of this assignment in past versions of the
class, both in Java and in Python. It is a good one, and students really
like it. Do not share your code with others. Do not obtain or look at a
copy of the earlier solution or a version being done by another student.
Such cheating helps no one, especially you, and it makes more unnecessary
work for us.
It is highly unlikely that your code for
this assignment will look exactly like someone else's. Once again, we
will be using Moss to check for instances of cheating and plagiarism.
Anyone caught copying code will be prosecuted, with the end result
perhaps being to fail the course.
Collaboration Policy
You may do this assignment with one other person. If you are going to work
together, then 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, you may not look at anyone
else's code or show your code to anyone else, in any form whatsoever.
Assignment Source Code
The first thing to do in this assignment is to download the zip file
A6.zip from this link. You should unzip it and
put the contents in a new directory. This zip file contains the following:
imager.py
-
This file contains the class Main , which is the primary class
for the application. It initializes all of the objects in the application.
It has methods that perform actions in response to the user clicking on
buttons in the application.
The file also contains subsidiary classes that manage dialogs that
appear in response to various user actions (loading a file, saving a file, etc.).
It also has the main driver class ImagerApp that initializes
the GUI and starts up the application.
imager.kv
-
This is a Kivy declaration file that describes the layout of the GUI
with buttons, panels, and other features. It does not do anything with
the buttons; it just describes their arrangement on the
screen, while imager.py has the methods that perform
actions when they are clicked.
image_panel.py
-
This module contains a single class ImagePanel. Objects
of this class have a single method: display , which is used to
draw an image on an ImagePanel.
image_processor.py
-
This file contains a single class ImageProcessor , which is the class that
has methods for manipulating an image. This is the only class that
you will need to modify.
image_array.py
-
This file contains a single class ImageArray . An object of this class
maintains an array of pixels in an image in row-major order. It has methods for
getting and setting a pixel in an image, and provides abstractions to treat the
image as either a 1-dimensional or a 2-dimensional list.
samples/
-
This is a subdirectory containing a number of images in .jpg and
.png format. You are welcome to add your own images if you like,
though you'll want to shrink them down pretty small; this program would be slow
on multi-megapixel images straight from a camera or phone.
The program is explained in more detail below. The
only class that you will need to modify is ImageProcessor . However,
be sure to look at the code for the entire project, as it is very illuminating, and it is
hard to understand what your code needs to do without understanding the overall plan.
Running the Application
In order to run the application, navigate to the directory containing these files
and type
python imager.py
This will import all the files and run the application. A window will open with
two versions of a default image, some buttons, and a text area, as shown below.
When you modify an image with this application, the left image will not change; it is showing
the original image.
The right image will change as you click buttons. The actions for the buttons Invert,
Transpose, Horizontal Reflect, and Rotate Right are already implemented.
Click on them to see what they do. The effects of the buttons are cumulative.
After any series of clicks, the Restore button returns the right image to its original state.
Python is not a speedy language, and pixels are numerous. When you click on the
buttons, you might notice that it takes a second or two for something to happen. (If you go
on to do image processing or scientific computing in Python, you will learn about libraries
that let you do these oprations efficiently, without having to explicitly loop over the
pixels.) To give
you some feedback on this process, the word Processing...
appears in the upper right corner whenever the application is doing something.
If this word does not go away after 5 seconds or so, then your program is likely stuck
and has a bug in it (e.g. you might have a while-loop that runs forever).
The remaining buttons are not implemented (Rotate Left does something, but not the
correct thing. Do you see why?). It is your task to write the code to make them work.
Getting Help
If you do not know where to start, or if you are completely lost, please see someone immediately.
This is a more complex assignment than the previous ones. You may talk to the course
instructors, a TA, or a consultant. See the
staff page for more information.
Do not wait until the last minute!
Understanding the ImagerApp Application
The Imager application has a few parts to it, and understanding how they work together will be
helpful to you as you complete this assignment.
Code Organization
The diagram to the right
shows how this application is organized. When we draw lines between two components, that
means that we are emphasizing that the two components must communicate with each other.
If the components are objects of a class, communication is typically done by one object
calling a method or accessing the data of the other. If there are no lines between two classes,
then we are not concerned about communication between objects of those two classes.
Thinking about which classes have to interact (and ideally, minimizing the number of these
interactions) is an important part of designing a program.
In this diagram, the first class that is instantiated and has its methods called is ImagerApp ;
this happens when you type
python imager.py
This class builds the GUI specified in the file imager.kv , and then
calls the config method of the Main class. This method
initializes the application, creating instances of the ImageArray and
ImageProcessor classes.
The Main class also listens for button presses and calls the appropriate
method, called a handler, to do something in response.
Most of the time, the handler methods call a method in ImageProcessor
to modify the displayed image, and then redraw the result. Some functionality,
such as loading and saving a file, is provided by pop-up windows that are supported
by other, minor classes in the imager module.
The image itself is maintained internally as a one-dimensional list of pixels
in the class ImageArray . As an image is conceptually a two-dimensional
list (width x height), the one-dimensional list stores the pixels in row-major order.
This means that it first stores the elements of row 0, then the elements of row 1,
then the elements of row 2, and so on. ImageArray
provides methods for manipulating the image, allowing access to the pixels of an
image row by row, column by column, or without regard to the order.
The class ImageProcessor provides methods for transforming an image
stored in an ImageArray object. It does not hold the image
data, but only manipulates it. As the lines in the diagram show, the
ImageProcessor knows
nothing about the GUI; it simply calculates. However, it does use the methods of
ImageArray to accomplish its work.
The file imager.kv is a Kivy file that specifies the placement of buttons,
labels with text, message area, and images on the screen. It also specifies the handlers
to be called in response to button clicks. This file is not written in Python, but
instead has an alternate format that is intended to make GUI layout simple. If you
are interested, we describe a bit of it below.
Finally, there is the class ImagePanel . This is just the view to display
the image. It has a single method called display which scales the
image to fit on screen and displays it. In this application, there are two instances
of ImagePanel , one for the original image on the left and one for the
modified image on the right. Calling the display method of either
one of them will cause its image to be updated, so that Python will redraw it.
The Class ImageArray
Abstractly, an image consists of a rectangular list of pixels (picture elements),
where each pixel entry is a tuple that describes the color pixel. We show a 3x4 list below,
with 3 rows and 4 columns, where each Eij is a pixel.
E00 E01 E02 E03
E10 E11 E12 E13
E20 E21 E22 E23
Compared to nested lists, simple one-dimensional lists are easier to hand off to the graphics
system in a single
chunk, and to read from or write to files. And, sometimes it is more convenient to think of
an image as just a list of pixels, without worrying about the 2D structure. Therefore, in practice,
images are stored as
one-dimensional lists.
The class ImageArray maintains the pixels in a list of
length r*c, where r is the number of rows, and c is the number of columns. For the 3-by-4 image
shown above, the list would
contain the elements in row-major order:
E00, E01, E02, E03, E10, E11, E12, E13, E20, E21, E22, E23
An object im of the class ImageArray maintains this list in
its hidden field _data . You should never access this field directly.
Instead, we have provided methods for accessing this field indirectly.
You can get an individual pixel of im with im.get_pixel(row,col) .
You can also use im.get_flat_pixel(n) . The former treats the image as a two-dimensional
list, while the latter treats it as a one-dimensional list. The one that you wish to use
depends upon your application. As you switch back and forth between these two methods, you
will see the benefit of encapsulation (i.e. hiding the list itself and working with it through
getters and setters).
In addition to the getters, you can change the image using im.set_pixel(row,col,pixel)
and im.set_flat_pixel(n,pixel) in the same way. Thus, to set the pixel in row
row and column col to a new value val , you would write
im.set_pixel(row,col,val) . There is also im.swap_pixels(r1,c1,r2,c2)
which can swap the pixels at two different locations. That is all you need to know to manipulate
images in this assignment. You never need to access the hidden field, and never need to be
concerned about how the list is laid out.
If you look at the class ImageArray , you will notice that it does not enforce all
preconditions, especially in the methods for getting and setting pixels. There are assert
statements there, but
they have been commented out. This is because of an unfortunate trade-off in programming;
assert statements make your program safer, but they also slow it down. This is particularly
true in image processing, where the methods containing these asserts are called thousands of times.
Uncomment the assert statements in set_flat_pixel and then try the Invert
button in the Imager application. See how much slower it is?
This is one reason we do not always enforce our preconditions. We put the preconditions in
comments, but rely on "the honor system", as asserts would slow down the program. But asserts
are really, really helpful with debugging, particularly when we have invariants, such as the
restriction that each component of a color value is an int in 0..255. This is why we put the
commented-out assert statements
in ImageArray . If you are having trouble with one of the functions, uncomment
these assert statements. Then, when you believe that everything is working fine, comment them
out again so that the program runs faster.
Pixels and Tuples
As we discussed in a previous assignment, your monitor uses the RGB (red-green-blue) color system
for images. 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 values in an RGB object
defined in the colormodel module. These were mutable objects where you could change
each of the color values.
In the ImageArray class we made a different design choice: the RGB colors are represented via
tuples, not RGB
objects. A tuple looks like a list, except that is defined without
square brackets, and is usually written with parentheses. For example,
x = (1, 3, 4, 5)
is a four element tuple. (A detail: the parentheses are not part of the tuple notation; it's
just traditional to include them to make the code more readable. You can
create the same tuple with just 1,3,4,5 .) Tuples are sequences and can be sliced
and indexed just like any other. Try
the following out in Python:
>>> x = (1, 3, 4, 5)
>>> x[0]
1
>>> x[1:3]
(3,4)
The only difference between a tuple and a list or object is that tuples are immutable.
You cannot
change the contents of a tuple, so an assignment like x[0] = 10 ) will produce
an error. This means that in an ImageArray, the pixel values are immutable. You do not change
an image by modifying the contents of a pixel object; instead you replace the pixel with a new one,
using
the setter methods in ImageArray ).
Speed is one of the reasons why we chose to use tuples to represent pixels. We could
have converted these tuples to RGB objects to make them more familiar. However,
like assert statements, this conversion would slow down image processing. So we want you to
deal with tuples directly.
The Class ImagePanel
An ImagePanel object maintains two data items: a kivy.uix.widget.Widget
object to display the image, and a reference to an ImageArray object. These items
are supplied to the constructor when the ImagePanel object is created.
The display method is called to display an image on the Widget. If given an
argument, it uses the new ImageArray object; otherwise it uses the one provided
to the constructor. It constructs a "texture" from the image, calculates its proper size and
placement, and applies the texture to the Widget. This is why we refer to ImagePanel
as a view class; it does nothing but draw the image.
How does one learn to write all this code properly? When faced with doing something like this,
most people will read the API specifications (located at kivy.org) and then
start with other programs that do something similar and modify them to fit their needs.
The Module imager.kv
All of the other
UI related components are defined in the file imager.kv . This is a Kivy file.
It is a simple way to lay out buttons, sliders and text boxes that cuts down on the amount
of Python code that you have to write. If you have ever done any work with web pages, this
file is a lot like a CSS file. It defines the look of your application, so that you only
need to write Python for the parts that do things.
We are not going to teach the Kivy language in this course, but it is not too difficult if
you simply want to understand what is going on. You start with the name of a Kivy class
in angle brackets (all of these classes are a subclass of kivy.uix.widget.Widget ,
which is used to draw something). Underneath you indent pairs of the form
attribute_name: attribute_value
On start up, Kivy creates such an object for each of these classes and initializes its
attributes to be the values you provided.
In addition, you will notice that sometimes a class is indented inside another class in
imager.kv . This means that it will draw that object inside of the other.
This how you align objects on the screen. For example, consider the following lines
from imager.kv :
BoxLayout:
orientation: 'vertical'
size_hint_x: .5
Button:
text: 'Restore'
on_release: root.do(root.image_processor.restore)
Button:
text: 'Invert'
on_release: root.do(root.image_processor.invert)
This means that we create a BoxLayout object which has attribute orientation
(we ignore attribute size_hint_x for now).
Since the value of this attribute is 'vertical', everything
drawn inside this object is arranged vertically. The Button objects are all
indented underneath, so they are drawn inside (and hence arranged vertically).
You will also notice in the example above that each Button object has an attribute
on_release with a method call following it. This is how we hook up the imager.kv
file with our Python code. These are the handlers that specify what happens when you press a button.
The Class Main
The class Main creates all of the other objects,
including the ImageProcessor object. It also implements some functionality that
is not strictly image manipulation, such as file I/O and calling display to
redraw the screen.
You will notice several other classes in both imager.py and imager.kv .
These minor classes define things like pop-up windows to load and save files. In each case,
the view is specified in imager.kv while the controller is specified in imager.py .
A few of these controller classes are essentially empty; that is because we are inheriting
the controller functionality from some base class, and we just needed to subclass it to make
sure that the controller and view had the same name.
The Class ImageProcessor
This class provides all the methods for manipulating the image given to it as an ImageArray
in the constructor. The constructor stores the image in the (immutable) property original
and stores a copy of it in the (mutable) property current .
As the image is manipulated, the object in current changes. It can be restored to its
original state by copying the property original to current . That is
what the procedure restore does. Note that restore actually creates
a new object; it is not sufficient to just assign original to current .
If we did that, both would point to the same object, so that whenever current changes,
original would change also. It is necessary to make a "deep" copy.
Other methods in this class implement the various handlers for the buttons. When a button
is clicked, the application calls the appropriate handler. The procedures invert ,
horizReflect , transpose , and restore are provided for you.
The procedure invert retrieves each pixel, computes its color complement, and places the new pixel back into
current .
Your goal in this assignment is to implement the procedures corresponding to the other buttons.
Assignment Instructions
The hardest part of this assignment is simply understanding how all of the classes fit
together. However, the only class that you need to modify is ImageProcessor .
Looking at this class, you will see that several methods are already complete, while
others are just stubs. For this assignment, you need to implement the following
six methods:
While working on these methods, you may find that you occasionally want to introduce
new helper methods. This is fine, and is actually expected for some of the tasks below.
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, because it helps you as you write the program. This is standard practice in this
course. It is a severe error not to do so, and points will be deducted for missing
or inappropriate specifications.
You need not write loop invariants, and if you do, we will not grade them. But we encourage
you to write them for each loop so that you get used to thinking in terms of the invariant.
Look at the implemented methods in ImageProcessor to see what a loop invariant
might look like.
Testing Guidelines
Before you write any code, you should be aware of our guidelines for testing. Using Python
unit tests is difficult because it is not easy to access a picture. You do not have to use one.
But this means you will need other creative ways of testing and debugging your code, such as
print statements.
If you add print statements to your code, please remember to remove them before submitting.
We will deduct points if you do not remove them.
Task 1. vertReflect
This method should reflect the image about a horizontal line through the middle of the image.
Look at the method horizReflect for inspiration, since it does something similar.
This should be relatively straightforward. Remember that loop invariants are not required, and
will not be graded. However, we will give you some feedback on them if you provide them.
Once you have implemented this method, you will notice that both the Vertical Reflect
and Rotate Left buttons now work. That is because the method rotateLeft
uses vertReflect as a helper. This demonstrates an a clever way to rotate an image.
Task 2. jail
This method is called when the user clicks the "Put in Jail" button. You can see the effect in
the picture to the right. This method draws a red boundary and vertical bars. This will happen
when you implement the method according to its specification, given
as a comment in the method. Be sure to follow that specification carefully.
We have given you helper method _drawHBar to draw a horizontal bar (note that we
have hidden it by naming it with a leading underscore; helper functions do not need to be
visible to other modules or classes). In
the same way, implement a helper method _drawVBar to draw a vertical bar. Do
not forget to include its specification in your code.
This is a problem where you have to be very careful with rounding, 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 finished, open a picture, click the buttons Put in Jail, Transpose,
Put in Jail, and Transpose again for a nice effect.
Task 3. monochromify
In this method, you will change the image from color to either grayscale or sepia. The choice
depends on the value of the integer parameter color , which is either 0 to indicate
grayscale or 1 to indicate sepia. However, do not use integer constants 0 and 1. Instead
use the names GRAY and SEPIA , which are already defined in image_processor.py to be 0 and 1,
respectively. There are a couple of good reasons for this. First, using mnemonic names rather than
the actual values makes the program more readable, because the names indicate the intended
meaning. Second, if you ever decide to change the representation in the future
(say to 3 and 4), you can do so in one place without having to go through the whole program
searching for (the right) occurrences of 0 and 1.
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:
brightness = 0.3 * red + 0.6 * green + 0.1 * blue
For grayscale, you should set each of the three color components (red, green, and blue)
to the same value, int(brightness) .
Sepia toning 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, get the color value for each pixel, recompute a new color value, and set the pixel to that
color. Look at procedure invert as well as the
discussion on tuples to see how this is done.
Task 4. 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.
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 methods get_pixel
and set_pixel in the class ImageArray , which treat
the array as a 2-dimensional list, rather than get_flat_pixel
and set_flat_pixel , which treat it like a 1-dimensional list. A flat list was
fine for invert and monochromify , but that was because the
row and column did not matter in those methods.
Task 5. Blurring, two flavors
The last part of the assignment is the most involved. A very common operation on images
is blurring: to reduce noise, to simulate an out-of-focus background, or to reduce distracting
detail in an unimportant part of an image. In this assignment you implement two kinds of
blur: a simple blur that removes detail everywhere and a fancier (though still simple) blur
known as bilateral filtering that is designed to remove fine details without blurring
important features.
Simple blur
This method works like the blur tool in Photoshop. This method replaces each pixel of the
current image by the average of itself and its eight neighbors (see figure at right). The averages must be taken
separately for each
color component. Thus an interior pixel's red component will be replaced by the average
of the red components of that pixel and its eight neighbors, and similarly for the blue
and green components. To simplify things, for this assignment do not worry about the
pixels in the first and last column and row, which don't have a full complement of eight
neighboring pixels; just leave them alone.
As in transpose , the method will have two nested loops, and each iteration
of the inner loop will process one pixel. Do not write all the code to process a pixel
in the inner loop. Instead, write a helper method that processes one pixel, and call
this from the body of the inner loop. This is done to keep each method simple and short.
Also, you can initially stub in the helper method (e.g. have it set the pixel to
carnelian, (179, 27, 27)) to facilitate constructing and testing one part of your
code at a time.
Even if you do it correctly, this method is slow. Our solution takes 3-4 seconds on
each of the sample images. Keep this in mind if you choose to implement when you run this method.
Bilateral filter
Once you have the basic blur working, the bilateral filter is a small tweak to the same
code. The goal is to smooth out detail without blurring strong edges, and the idea is simple:
avoid averaging together pixels with very different values. The blur proceeds exactly as a
basic blur, except that a neighboring pixel is excluded from the average if the
difference between its pixel value and the center pixel's value exceeds some threshold. (The
name bilateral comes from the idea of selecting pixels to average according to two
criteria: they have to be close in position, and they have to be close in value.)
In order to decide which pixels to include, we need to have a way to measure difference
between two RGB pixels using a single number. In this assignment we use the maximum difference
across all three channels: that is, the largest of the difference in red value, difference in
green value, and difference in blue value. The differences are measured in absolute value;
for instance the red difference is abs[(red of center pixel) − (red of neighbor pixel)].
Don't forget to count how many pixels you add up in your average, so that you can divide by
that number when you compute the average pixel value.
Bilateral filtering is used for many things in digital photography; for one, it is used to
remove noise from grainy images without making them look blurry. Too much of this makes images
look odd—or artistic, depending on your view. Play with your implementation a little,
seeing what effect the threshold has (you change the threshold using the text box next to the
Bilateral button in the Imager window) and what happens when you apply the filter repeatedly.
Note that very
low thresholds (for instance, 0) exclude all pixels that are at all different from the
center pixel, resulting in an unchanged image, and very high thresholds (e.g. 255) don't
exclude any pixels, which reduces to computing the basic blur. A moderate threshold with
repeated application produces an almost painted look.
Finishing the Assignment
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:
- There are no tabs in the file, only spaces
- Classes are separated from each other by two blank lines.
- Methods are separated from each other by a single blank line.
- Lines are short enough that horizontal scrolling is not necessary (about 80 chars is long enough).
- The specifications for all of the methods and properties are complete.
- Specifications are immediately after the method header and indented.
Finally, at the top of image_processor.py you should have three single line
comments with (1) the module name, (2) your name(s) and netid(s), and (3) the date you
finished the assignment. We have been taking off points for people that keep forgetting
to do this.
Turning it In
Upload the file image_processor.py to CMS by
the due date: Saturday, April 13th at 11:59 pm.
Do not submit any files with the extension/suffix .pyc . It will help to set the preferences in your operating system so that
extensions always appear.
|