T-Th 9:05
or
T-Th 11:15
in Olin 155

CS 1110: Introduction to Computing Using Python

Fall 2012

Assignment 6:
Images

Due to CMS by Friday, November 16th at 11:59 pm

While the previous assignment was interesting, the types of things that you could do with images was pretty limited. In that assignment, all you could do was move the four corners of the image. Suppose you wanted to do the types of effects that you can do in Photoshop, such as blur an image or invert its colors (seen to the right). In that case you need to modify the image pixel by pixel.

We already saw in class that you can represent an image as a 2-dimensional list of color pixels. For purposes of efficiency, however, most graphics cards like to represent images as a 1-dimensional list in row-major order, which we describe below. Row-major order can be really confusing to beginners. Therefore, this assignment encapsulates the image's pixel list inside of a class called ImageArray. This class provides an abstraction that allows us to treat the list of pixels as either a one-dimensional or a two-dimensional list, depending on our application. So in addition to learning about images, you will also learn a lot about encapsulation.


Reading the Instructions

In the instructions below, we provide a lengthy aside on application design. We refer to certain parts of the application as model, view, or controller. This is the subject of one of the lectures from just before the second prelim. We have provided this discussion so you can see how to apply these concepts to a simple application like this one.

With that said, this discussion is very long, and you might find it a bit overwhelming. You do not need to follow all of it to do this assignment. If you find the section on application design confusing, or you just want to get started with the assignment, feel free to skip over it and start with the discussion of ImageArray instead.

Even skipping over the section on application design, these instructions are quite long. You should spend at least one evening simply reading these instructions while looking over the existing classes. Do not write any code until you understand how everything fits together.


Learning Objectives

This assignment has several important objectives.

  • It gives you practice with writing while-loops and using invariants.
    (While you could use for-loops for many of these methods, while-loops are preferred)
  • It gives you practice with writing loops to process both 1-dimensional and 2-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.
  • It introduces you to the concept of the model-view-controller pattern.

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. This assignment is due only nine days after the second prelim (Tuesday, November 6), and there is no room to grant extensions. There are seven methods in total. We recommend that you work on at least one method a day. The last two methods might need three days.


Academic Integrity

While this is the first time we have presented this assignment in Python, we have given it before in Java. 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.

As with the last assignment, 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.

In our application design, this is a controller module.

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.

In our application design, this file is essentially a view module.

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.

In our application design, this is a view module.

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.

In our application design, this is a controller module.

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.

In our application design, this is a model module.

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.

The program is explained in more detail below. The only class that you will need to modify is ImageProcessor. However, feel free to look at the code for the entire project, as it is very illuminating. While the view modules are hard to read (as they are in a language other than Python), you should be able to understand the controller and model modules with no problem.

All of this code comes with online documentation describing the interface and specifications.


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 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, 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 image processing is intensive. When you click on the buttons, you might notice that it takes a second or two for something to happen. To give you some feedback on this process, you will notice that the word Processing... will appear 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 instructor, a TA, or a consultant. See the staff page for more information. Do not wait until the last minute!


Understanding the ImagerApp Application

The application ImagerApp is a complicated one, and in this section we give you an idea of the various components. However, you do not actually need to understand all of these components to get started. Feel free to skip over to the discussion of ImageArray if you are impatient.


Advanced Application Design

Real software applications are not made from a single component. They are often composed of many components, perhaps even hundreds. When you have a program that is made up of several components, it is important to organize them in a logical and coherent way, so that it is clear what each component is responsible for and so that interactions between them are kept reasonable. The larger and more complicated the task of a program, the more important it is to have good organization.

The classic way of organizing applications—particularly those that sit on a single computer and do not need to connect to the internet—is using what is known as the model-view-controller pattern. In this pattern, every class is categorized as either a model, a view, or a controller. The roles of each are detailed below.

Model

A model is a class that holds the application data. It provides getters and setters (either via properties, or normal methods) to manipulate the data. It also provides useful additional methods to perform common tasks on the data. The Vector and Matrix classes that you worked on in the previous assignment are examples of models.

View

A view is a class that allows a human user to interact with the program. It displays information on the screen in human-understandable form using a graphical user interface (GUI). It also monitors user input, such as typing or moving/clicking a mouse. However, it does not tell the program what to do when the user performs input. It only keeps track of the fact that it happened.

The Window class that you used in the Turtle Assignment is a classic example of a view box. It displayed a window but did not actually do anything unless you added more features (like the Turtle).

Controller

Controllers are the heavy lifters that do everything else. They start up the program and instantiate the other classes (e.g., the view and model). They also provide methods that do something when the user performs some input. Finally, they also force the view to refresh the screen whenever it should be updated.

While you have not yet written a "pure" controller class in this course yet, the Turtle methods in Assignment 4 are are fairly close to what a controller does.


Code Organization

Now that you have been introduced to the model-view-controller concept, 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 classes, then we are not concerned about communication between objects of those other classes. This is another important feature in organizing code.

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 that can communicate with one another. The Main class also listens for button clicks and calls the appropriate method, called a handler, to do something in response to the click.

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. As a model class, ImageArray provides methods for manipulating the image, allowing a user to process the pixels of an image row by row, column by column, or without regard to the order.

The class ImageProcessor provides methods for transforming the image, which is maintained as an ImageArray object. It does not hold the image data, but only manipulates it. For this reason, it is categorized as a controller, not a model. As the lines in the diagram show, the ImageProcessor knows nothing about the GUI; it simply calculates. However, it does communicate with ImageArray.

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.

In reading the various classes, the format of the specification might see very strange to you. That is because we have formatted our specifications to be compatible with Sphinx, a tool for generating documentation web pages. We have used this to generate online documentation for this application.


The Class ImageArray

Images are actually stored in the Image objects provided by the PIL library. This library allows you to load images in any popular format, such as .jpg or .png. Exactly how the image is stored in an Image object is of no concern to us. Abstractly, the 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

This looks a lot like a two-dimensional list, which we have already talked about in class. However, one-dimensional lists are a lot faster to process, and image processing can be very slow. Therefore, in practice, images are stored as one-dimensional lists.

The class ImageArray maintains the pixels in a one-dimensional 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 the individual pixel of im with im.getPixel(row,col). You can also use im.getFlatPixel(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 (e.g. hiding the field and forcing you to go through getters and setters).

In addition to the getters, you can change the image using im.setPixel(row,col,pixel) and im.setFlatPixel(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.setPixel(row,col,val). There is also im.swapPixels(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 understand how row-major representation works.

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 setFlatPixel 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. A common feature in many languages (other than Python) is to have a "release" mode and a "debug" mode. In "debug" mode, the language executes the assert statements, while it skips over them in "release" mode.

Python does not have this advantage, which 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 PIL Image library, the RGB colors are represented via tuples, not RGB objects. A tuple looks like a list, except that is defined with parentheses instead of square brackets. For example,

  x = (1, 3, 4, 5)
is a four element tuple. Tuples are sequences and can be sliced 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 is that tuples are immutable. You cannot change the contents of a tuple, so any assignment (e.g. x[0] = 10) will produce an error.

Tuples are a natural way of representing color values. You just use a 3-element tuple with values 0..255. Indeed, in Assignment 3, some of you accidentally returned a tuple for your functions rather than an RGB object. We preferred the RGB objects because they had assert statements to check the invariants for each color object. Tuples provide no such guarantees.

The important thing about images is that the pixel values are immutable. You do not change an image by modifying the contents of a pixel object. You need to make a new pixel value and store that new value in the right place in the image (using the setter methods in ImageArray). This is one of the reasons why the PIL image library uses 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

The class ImagePanel is the only explicitly-defined view class. All of the other components of the view 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 is the primary controller. It 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.

The class Main is considered a controller class because all of the methods are handler methods that react to the user doing something, like pressing a button, or saving a file. There is no code here for arranging where buttons go, what they look like, or what their names are. You will note, however, that there is a class called Main inside of imager.kv, which does all of these things. This is not a coincidence. The purpose of imager.kv is, for each class in imager.py, to define the view corresponding to the controller class of the same name. This is the underlying design philosophy of Kivy: define the view in the Kivy language and write the controllers in Python.

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:

The method fuzzify is not required. It is just presented as an optional challenge. We will not grade it even if you do complete it.

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. You are well-aware of using traces and watches now; use them to your advantage.

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 alternate way of rotating an image that is different than what we did in the previous assignment.


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; 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 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 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 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 method getPixel and setPixel in the class ImageArray. These methods treat the array as a 2-dimensional list. Do not use the methods getFlatPixel and setFlatPixel, which treat it like a 1-dimensional list. That was fine for invert and monochromify, but that was because the row and column did not matter in those methods.


Task 5. Steganography

The last official part of the assignment is the most involved. Steganography, according to Wikipedia, is "the art and science of writing hidden messages in such a way that no one apart from the intended recipient even realizes there is a hidden message." This is different from cryptography, where the existence of the message is not disguised but the content is obscured. Quite often, steganography deals with messages hidden in pictures.

To hide a message, each character of the message is hidden in one or two pixels of an image by modifying the red, green, and blue values of the pixel(s) so slightly that the change is not noticeable to the eye.

Each character is represented using the American Standard Code for Information Interchange (ASCII) as a three-digit integer. We allow only ASCII characters; all the obvious characters you can type on your keyboard are ASCII characters, but it does not support certain foreign characters or Kanji.

For the normal letters, digits, and other keyboard characters like '$' and '@', you can convert back and forth between a character and its ASCII representation using the Python built-in functions ord and chr. For example, ord('k') evaluates to 107, and chr(107) evaluates to 'k'.

We can hide the character 'k' in a pixel whose RGB values are 199, 222, and 142 by changing the least significant digit of each color component to one of the digits of 107, the integer representation of 'k':

Original Pixel Pixel with 'k' hidden
Red Green Blue hide 'k', which is 107 Red Green Blue
199 222 142 191 220 147

This change in each pixel is so slight that it is imperceptible to the human eye.

Decoding the message, the reverse process, requires extracting the last digit of each color component of the pixel and forming an ASCII value from the three extracted digits, then converting that ASCII value to a character. In the above example, we would extract the digits 1, 0, and 7 from the RBG components of the pixel, put them together to form 107, and then apply chr to this value to obtain 'k'. Extracting the message does not change the image. The message stays in the image forever.

Three Problems for You to Solve

You are to write code to hide characters of a message text in the pixels of an image in row-major order, starting with pixel 0, 1, 2, ... Think about the following issues and solve them.

First, you need some way to recognize that the image actually contains a message. Thus, you need to hide characters in pixels 0, 1, 2, ... that have little chance of appearing in a real image. You cannot be sure that an image without a message does not start with the pixels resulting from hiding the characters, but the chances should be extremely small. You should have a beginning marker of at least two pixels that indicates that this file contains a message.

Next, you have to know where the message ends. You can do this in several ways. You can hide the length of the message in the first pixels in some way (how many pixels can that take?). You can also hide some unused marker at the end of the message. Or you can use some other scheme. You may assume that the message has fewer than one million characters, but you must prepared for the message to contain any sequence of characters, including punctuation.

Finally, the largest value of a color component (e.g. blue) is 255. Suppose the blue component is 252 and you try to hide 107 in this pixel. In this case, the blue component would be changed to 257, but this impossible because a color component can be at most 255. Think about this problem and come up with at least two ways to solve it. Implement one of them.

As you can see, this part of the assignment is less defined than the previous ones. You get to come up with the solutions to some problems yourself. Part of this assignment will be to document and discuss your solutions.

Documenting Your Approach

You should decide how you will solve the problems mentioned above. As you design and implement this part, write a short essay that documents at least two solutions to each of the three problems mentioned above. By "essay", we mean a few paragraphs, not a term paper!

You should write this essay in a separate text file called essay.txt. If you do not know what a text file is, it is just normal text document without any of the fonts and other formatting that you would get from a program like Microsoft Word. Komodo can make text documents as well as Python modules; just save the file with extension .txt. You will upload this text file along with your Python code when you are done.

This essay should indicate what your solutions are and discuss their advantages and disadvantages. Advantages could be: fewest number of pixels used in hiding an image, least time spent hiding a message, or something similar.

In your essay, do not describe your code (e.g. what the if-statements are, and what assignments are used). Your reasoning should be at a higher level than that. Use English, not code. At the end of the essay, state precisely the format of a hidden message. Tell us what the beginning marker is if there is one, how you encode length, and so on.

You should feel free to discuss this part of the problem with the course staff. They will not tell you how to solve these problems, but they will discuss your ideas with you. All of them have done this assignment before, back when it was in Java. And this is a case where the Java/Python differences do not matter at all.

Complete the Method getPixels(n)

Before you do any steganography at all, complete and test the function getPixels(n) in the class ImageProcessor. The purpose of this extra method is to help with debugging the method hide. From experience, we know that students who do not test hide properly can have great difficulty with this part of the assignment. After writing this function, you can insert this call anywhere in ImageProcessor to print the first 21 pixels of the current image:

        print self.getPixels(21)
This should result in output similar to the following (this is for the image goldhill.png, not the default goldhill.jpg):
Pixels of current image:
     000:025:006 014:028:013 029:025:016 038:027:021 044:037:027
     048:048:036 050:052:039 053:055:042 048:048:036 050:048:036
     051:049:037 053:049:038 056:049:041 055:051:042 052:052:042
     052:054:041 053:053:041 056:054:041 056:054:041 057:055:042
     058:056:043

You will notice in the output above that we padded all of the color values with zeroes to make them three digits. This makes everything "line up" so it is easier to figure out what the problem is. You can get this output yourself by using the helper method _pixel2str. With that said, you do not need to use this helper method if you do not want to. Just follow the specification.

One way to test that your getPixels is working is to try it out on the provided image whiteout.png. This is an all-white image, so the pixel values should all be 255:255:255

Complete the Methods hide and reveal

You should complete the body of the methods hide and reveal in the class ImageProcessor. These two methods should hide a message and reveal the message in the image. When you design reveal, make sure it attempts to extract the message only if its presence is detected.

Feel free to introduce other helper methods as needed. For example, we have provided a helper method called decode, which takes a pixel position p and extracts a 3-digit number from it, using the encoding that we suggested above. This suggests that you might want to create a helper method called encode, which encodes a number into a pixel. The exact specification of such a helper is up to you (if you start with our specification, be sure to modify it as appropriate for your code).

You will also notice that we use the method getFlatPixel in decode. That is because it is very natural to think of an image as 1-dimensional list when encoding text. While an image is 2-dimensional arrange of values, text is not. Hence, once again, we see the advantage of encapsulation in ImageArray, allowing us to access the data how we want for the particular application.

Testing hide and reveal

Debugging hide and reveal can be difficult. We encourage the use of helper methods to keep your code simple and short. You get to decide which ones to write as you design and implement this assignment. Be sure to specify each method appropriately in a comment.

Do not assume that you can debug simply by calling hide and then reveal to see whether the message comes out correctly. Instead, write and debug hide and then write and debug reveal. Start with short messages to hide (up to three characters) and use getPixels to check that the pixels contain the message. Remember to delete the starting message:

<type hidden message here>
before hiding your message. Otherwise, you will encode that message as well.

When hide and reveal are done, try hiding and revealing a long message (e.g. 1000, 2000, or 5000 characters). This might be a little difficult. It appears that Kivy support for external clipboards (e.g. copying from one program and pasting in another) is broken in Macintosh, and unreliable on some Windows machines. If this is the case, we have introduced an "easter egg" to make things easier.

Go into imager.kv and look at lines 224-228. You will notice that they are commented out (with a #, just as in Python). Remove the comment symbols so that <Button>: is now aligned under <Widget> above it. Relaunch the application and you will now see a Load Text button like the one shown below (look at the lower right). Press this button to load the contents of a text file into the text box.

When you test your program on large messages, do you notice any difference in the time to hide and the time to reveal? Can you figure out why? We will discuss this in class at the appropriate time.

About the Save Button

Clicking the Save button saves the image in the specified directory with the filename you enter. The message is saved in .png format, which is a lossless format. Saving in .jpg format would not work, because doing so tries to compress the image, which would result in less space but would also clobber your hidden message. With .png, you can hide a message, save, and then reload the image with the message still there.


Optional: fuzzify

You are not required to implement fuzzify. It is simply an extra challenge for those of you who want one.

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 neighbors. Note that pixels in the corners have three neighbors, pixels on the edges but not on the corners have five, and interior pixels (those not on an edge) have eight. 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.

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 the 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 this method.


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:

  1. There are no tabs in the file, only spaces
  2. Classes are separated from each other by two blank lines.
  3. Methods are separated from each other by a single blank line.
  4. Class contents are ordered as follows: fields, properties, constructor, operators, methods.
  5. Lines are short enough that horizontal scrolling is not necessary (about 80 chars is long enough).
  6. The specifications for all of the methods and properties are complete.
  7. Specifications are immediately after the method header and indented.

There is a new coding convention which we have not yet explicitly stated. Non-hidden methods should be in camelCase. That is, compound words are not separated by underscores, but instead are indicated by making the first letter of each word (after the first) upper case. This allows us to distinguish methods from functions, as functions are all lower case separated by underscores. You may notice that we break this convention a few times in the class Main in module imager. You are allowed to break this convention with hidden helper methods, but not in methods that are meant to be accessed outside of the module.

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 files image_processor.py and essay.txt to CMS by the due date: Friday, November 16th at 11:59 pm. Do not forget the essay. Do not submit any files with the extension/suffix .pyc (or .doc or .pdf). It will help to set the preferences in your operating system so that extensions always appear.

Survey

In addition to turning in the assignment, we ask that each of you individually (even if you worked in a group) complete the survey posted in CMS. Once again, the survey will ask about things such as how long you spent on the assignment, your impression of the difficulty, and what could be done to improve it. Please try to complete the survey within a day of turning in this assignment. Remember that participation in surveys comprise 1% of your final grade.