CS 211, 004/006
Project 5
due Tuesday, December 4th at 11:59pm


You are given a stack of photos corresponding to various places, and a second stack of photos which you're unsure about where they were taken. Your goal will be to guess where the photos were taken by comparing them for similarity to the photos you are sure about. Unlike in previous assignments, you will only be given the bare interfaces which we can use to interact with your program, while most of the implementation is yours to decide. Additionally, you will have the chance to exercise the use of file I/O and recursion.

Overview:

  1. You are provided with the Photo interface. It defines methods for loading a photo from a file, and retreiving size and pixel information.
  2. You should create an enum called Pixel which defines the values WHITE, GRAY, and BLACK.
  3. You are also provided three interfaces called PhotoLoader, PhotoMatcher, and PhotoLocator which have methods for working with images: loading sets of images, finding the quality of a match between two images, and finding the best match for a set of images.
  4. You will create all necessary classes to implement the functionality of the interfaces above. In particular, you must create an instantiable class called PhotoProcessor which implements PhotoLoader, PhotoMatcher, and PhotoLocator.
  5. You are provided with the rules for scoring the quality of a match between photos in order to determine where they have most likely been photographed.
  6. You are provided sample image data and a tester to test your code. Also, you are given a utility for converting images to text files for use in this project (it may not be necessary).
  7. Document all public methods, classes, interfaces and enumerations using JavaDoc style comments.
  8. Finally, you are given an (optional) ExtraCredit interface to implement which uses a more flexible notion of matching images.
So far, we have worked on projects where the classes and methods to use have been provided for you. In the typical job, however, we're far more likely to be given a task to accomplish and asked to find a way to do it. This assignment will proceed in that direction. We know what we want to accomplish, and we are given the interfaces which we want to provide (because we need to be clear about how our code interacts with the rest of the world), but the full solution is on you.

This project is a photo processor, which takes a number of photos and tries to determine where they were taken. It's typical of a kind of app you might expect to find on a social networking site. You will be able to load photos from files, and then determine their similarity. Based on the similarity, you will try to match the photos with a set of background images, to determine which ones were taken where.

As a simplifying assumption, all photos are three colors: BLACK, GRAY, and WHITE. Furthermore, assume that all photos are the same dimensions (for extra credit, you can handle photos which can be different dimensions and which are not necessarily perfectly aligned). The quality of a match is based on the percent of pixels which match or somewhat match (more on that later).

You will need to implement four interfaces, at the very least: Photo, which lets you load a photo and read its pixel data; plus the three PhotoProcessor-related interfaces providing the methods necessary to match pairs of photos.

Rules

  1. This project is an individual effort; the Honor Code applies
  2. You may import any standard Java library if you see the need.
  3. All fields must be declared private or protected. This will be verified manually.
  4. You must have classes which implement the provided interfaces. In particular, you must have a PhotoProcessor class which implements PhotoLoader, PhotoMatcher, and PhotoLocator.
  5. You are responsible for designing your own classes and methods. Any functionality beyond what is required by the interfaces is at your own descretion.
  6. Any class, interface, and enumeration, as well as any public method, must be documented using JavaDoc style of comments.

Pixel:
public enum Pixel

This enumeration must contain the three values, BLACK, GRAY, and WHITE.

Photo:
public interface Photo

Photos are made up of a rectangle of 3-color (black/gray/white) pixels. The upper left corner of the photo is (0, 0). The x-value increases when moving to the right, and the y-value increases going downwards. When the load function is called with a filename, a photo is read from a text file. Once an image is loaded, the interface's methods can be used to access the image's pixel data. Below is an example of what a photo file might look like:

          
  . ...   
   ..x..  
  ..xxx.. 
  ...x..  
     .    
  ... . .x
     .x.x 
    .x    
    .x    

Tip 1: In order to be able to test your Photo, your PhotoProcessor must be partially functional as well, because it's the only way we can load photos using your code.
Tip 2: Recall that you can use a Scanner to read from a file as well. You just have to make sure that when you first create the Scanner object, you should open it with a File or a FileInputStream.

PhotoLoader:
public interface PhotoLoader

This interface provides all of the image loading functionality which we need for this project. You will also need to write a class called PhotoProcessor which implements this functionality, so that we have something which we can instantiate and use. The PhotoProcessor must have a default constructor (i.e. it must be possible to instantiate it with no arguments).

The role of this interface is for loading images. It should be possible to load a set of background images, as well as a set of foreground images. The background images represent a set of locations, and the goal is to figure out which foreground images correspond to which locations. A list of background images will be specified by a file. So for example, we may have a file bgplaces.txt containing:

johnson_center.txt
washington_monument.txt
fair_oaks_mall.txt
The three lines of this file are the names of three photo files corresponding to different locations. If we call the load function with the argument "bgplaces.txt", the load function will look inside this file, take the three filenames there, and load each of the three as photos. A similar process would be used for foreground images. Once the photo files are loaded, we also have methods to retrieve the photos by name or as a collection.

Tip 1: a Collection is an interface which is implemented by a number of different types of lists. An ArrayList is a Collection and a HashMap contains a Collection, for instance. In order to get back an array from a collection (if you prefer to work with it that way, you can use toArray(). For example:

ArrayList<String> list = new ArrayList<String>();
// build the list
String[] array = list.toArray(new String[0]);
Tip 2: the getPhoto() method should retrieve photos whether they have already been loaded or not. It may make sense to keep some kind of list of photos which have already been loaded (as foreground or background photos, or independently), and if it's not already part of that list, then load it separately.

PhotoMatcher:
public interface PhotoMatcher

Another role of the PhotoProcessor (which implements this interface) is to find matches between photos. Given two photos, the quality of a match is defined as follows:

For a pair of pixels, if they both have the same value (WHITE and WHITE, GRAY and GRAY, or BLACK and BLACK), then the resulting score is 1.0.
If exactly one of the pixels is GRAY (thus, the other one is either WHITE or BLACK), then the resulting score is 0.5.
Otherwise the resulting score is 0.0.

The quality of the match is the average of the score of every pair of corresponding pixels between the two images (i.e. the (0, 0) pixel in the first image vs the (0, 0) pixel in the second image, the (1, 0) pixel vs the (1, 0) pixel, etc). Assume that both images are the same size and and are aligned with one another.

As an example, suppose that we have the following background picture:

xxxxx
xx.xx
x. .x
xx.xx
xxxxx
Together with the following foreground picture:
xxxxx
x. .x
x.x.x
x. .x
xxxxx
Out of a total of 25 pixels, the two pictures are identical in 18 of them (a score of 1.0 each). In 4 pixels, the background is black while the foreground is gray (a score of 0.5 each). In 2 pixels, the background is gray while the foreground is white (a score of 0.5 each). In 1 pixel, the background is white while the foreground is black (a score of 0.0). Thus, the net score is (18*1.0 + 4*0.5 + 2*0.5 + 1*0.0)/25 = 0.84.

PhotoLocator:
public interface PhotoLocator

Finally, you will need to be able to match your foreground photos up with the background photos to see which came from where. Assume that the number of foreground photos is no greater than the number of background photos, and that at most one foreground photo matches with each background photo. The goal is to find the mapping of foreground-to-background photos which has the best overall score (i.e. mapping produces the largest possible sum of quality scores). This interface should be implemented by PhotoProcessor as well.

Since only one foreground photo is allowed for every background photo, you will most likey need to backtrack to find the best solution. This sort of problem lends itself to a recursive solution (try to match a pair, then reduce it to the subproblem of matching the remaining photos). In practice, this would be a very slow process if a many photos are involved, but as long as there are just a few, it should be doable. Also, consider saving or precomputing your match scores to save some time.

As an example, suppose that we have two background photos, bg1 and bg2, and two foreground photos, fg1 and fg2. Let's say that fg1 has a match score of 0.93 with bg1 and 0.31 with bg2. Meanwhile, fg2 has a score of 0.13 with bg1 and 0.72 with bg2. Then the best match will place fg1 with bg1 and fg2 with bg2. The overall score of this choice is 0.93 + 0.72 = 1.65, which is far better than the alternative choice's overall score, 0.31 + 0.13 = 0.44

Let's try a slightly different example. Suppose that fg1 with bg1 has a score of 0.85, while with bg2 it has a score of 0.65. Meanwhile, suppose that fg2 has a score of 0.80 with bg1 and 0.12 with bg2. Then the matching of fg1 with bg1 and fg2 with bg2 has an overall score of 0.85 + 0.12 = 0.97, while matching fg1 with bg2 and fg2 with bg1 has an overall score of 0.65 + 0.80 = 1.45. Thus, the latter matching is better, despite the fact that the best independent matching for fg1 is bg1.

If there are three foreground and three background photos, then there are six possible matchings to pick from, and so on.

Tip 1: if the number of photos is not too large, then a recursive solution may make sense. For example, consider trying to match foreground pictures fg1, fg2, and fg3 to background pictures bg1, bg2, and bg3. We could try matching fg1 with bg1, then use the recursive subproblem of finding the best match of fg2 and fg3 with bg2 and bg3. After doing that, we can try matching fg1 with bg2 and recursively finding the best match of fg2 and fg3 with bg1 and bg3. And so on.

ExtraCredit: (optional, extra credit)
public interface ExtraCredit

JavaDoc:
All of your public methods, classes, interfaces, and enumerations must be documented using JavaDoc-style comments. This includes declaring method parameters and return types.

Example:

> import java.util.HashMap;
> PhotoProcessor pp = new PhotoProcessor();
> pp.loadFGManifest("fgphotos.txt");
> pp.loadBGManifest("bgplaces.txt");
> System.out.println(pp.getFGPhotos().size());
3
> System.out.println(pp.getBGPhotos().size());
3
> HashMap<String,String> best = new HashMap<String,String>();
> System.out.println(best);
{}
> pp.getBestMatch(best);
> System.out.println(best);
{p3.txt=washington_monument.txt, p2.txt=johnson_center.txt, p1.txt=fair_oaks_mall.txt}
> Photo fg1 = pp.getPhoto("p1.txt");
> Photo bg1 = pp.getPhoto("johnson_center.txt");
> System.out.println(pp.getMatch(fg1, bg1));
0.818115234375
> System.out.println(pp.getMatch(fg1, fg1));
1.0
> Photo test = pp.getPhoto("testphoto.txt");
> System.out.println(test.getName());
testphoto.txt
> System.out.println(test.getWidth());
5
> System.out.println(test.getHeight());
5
> System.out.println(test.getPixel(3,3));
WHITE
> System.out.println(test.getPixel(0,4));
GRAY

Downloads:

The following downloads will help in preparing the project:

Grading:

Roughly half of the score comes from automatic testing. The score breakdown will be as follows:

Submission:

Submission instructions are as follows.

  1. Let xxx be your lab section number (one of 213-220), and let yyyyyyyy be your GMU userid. Your userid is your patriot pass login name, not your G number. Create the directory xxx_yyyyyyyy_P5/
  2. Place all files corresponding to the classes/interfaces you've written in the directory you've just created.
  3. Create the file ID.txt in the format shown below, containing your name, userid, G#, lecture section and lab section, and add it to the directory.

    Full Name: Donald Knuth
    userID: dknuth
    G#: 00123456
    Lecture section: 004
    Lab section: 213

  4. compress the folder and its contents into a .zip file, and upload the file to Blackboard.