The goal of this project is to give you the opportunity to play around with more fun manipulations of 2-dimensional grids, in this case photographs. You are provided with a library that allows you to read and write photographs as well as to manipulate them and show them on the screen. The methods available to you are described in more detail below.
Unlike other projects so far, in this project you will have to fully define the methods you need to create - we no longer provide a method stub within which you fill in your code. To make this process easier we have completely implemented one method that you can use as a template for your own implementation. Likewise, the driver code only contains an example on how to use the interface - it is fully up to you how you modify it. Note that the driver code (the DrivePhotograph.java file) is not tested as part of the submission - it is there to allow you to see the results of your work, and to play around with the photographs.
Please read carefully all the text below before starting your project.
Note that unlike other projects you can try to release test your project even if the public tests have not all been completed.
A bit about images/photographs
The way we view photographs for the purpose of this project is as two-dimensional grids of pixels. The pixels themselves contain information about the color shown at that particular location in the image. The color is represented as three integers, representing the intensity of the light in three color channels - Red, Green, and Blue (RGB). The intensity can only take values between 0 and 255 (8 bits of resolution).
For a better understanding of the RGB model see here: https://en.wikipedia.org/wiki/RGB_color_model .
Note that color combinations don't exactly work the way you remember from art class. For example, setting the R, G, and B values to their maximum value (255) yields the color white. Different scales of gray can be obtained by setting all three values to the same number less than 255.
One concept that will be useful for your project is intensity - the intensity of the light coming from a pixel. It is not immediately obvious how to define the intensity of a colored pixel. In principle you could assume the intensity of a pixel to simply be the average intensity of the three channels. For a variety of reasons related to our color perception, however, the different colors have to be scaled differently. For the purpose of this project we will use the following definition of intensity:
I = 0.2989 * R + 0.587 * G + 0.114 * B
where R, G, and B are the corresponding values of the red, green, and blue colors. For a longer discussion of the origin of these values see: https://en.wikipedia.org/wiki/Luma_%28video%29
You will need to use this transformation in both the conversion to grayscale and to implement the edge detection. I recommend you create a method getIntensity that either takes a Pixel as a parameter and returns an integer intensity value, or that takes three integers (the values for red, green, and blue) and returns an integer intensity value.
The photograph library
Your starter code should contain a file called photograph.jar that contains several utilities for interacting with photographs. Below is a description of the main classes and methods you will need to use for the project.
This class holds the photograph information.
You can create a new photograph using an image from a file using the statement:
Photograph photo = new Photograph("Pictures/arib-calibrated.jpg");
The file arib-calibrated.jpg is a monitor calibration pattern provided to you in the folder Pictures within the starter code. Also available in that folder is the file Littoralis_Female_Face.JPG - which is a photograph of a diamondback terrapin. These are good pictures to start playing with, but you can also put your favorite pictures in this folder.
You can also create a new photograph using an image from a URL using the statement:
Photograph photo = new Photograph("http://www.cs.umd.edu/~pugh/testudo.jpg");
Finally, you can create a blank photograph of given width and height using the statement:
int height = 20, width=30;
Photograph photo = new Photograph(width, height);
Getting the size of a photograph
Like in the RectangularGrid, you can get the size of a photograph using the methods:
public static int getHt(); // reads the height of a photograph
public static int getWd(); // reads the width of a photograph
int width = photo.getWd();
Getting a specific pixel from a photograph
To read a specific pixel from a photograph you can use the method:
public static Pixel getPixel(int x, int y); // retrieve pixel at coordinates x and y
int x = 320, y=215;
Pixel p = photo.getPixel(x, y);
Note that the point of coordinates x = 0, y = 0 is at the top left of the photograph, the same as in the case of the flag project but unlike the convention you may be used to from geometry/math classes.
Setting a specific pixel in a photograph
You can 'paint' a specific location within a photograph using the method:
public static void setPixel(int x, int y, Pixel p); // places pixel p at coordinates x and y
int x = 320, y = 215;
Pixel p = new Pixel(0, 0, 0); // a black pixel
photo.setPixel(x, y, p)
The Photograph class also implements the equals method which has the same semantic as you are used to in the context of the String class. This method will be useful to you for writing JUnit tests for your project. You can call it explicitly to compare two photographs:
System.out.println("Photos are the same");
Or it can be called implicitly by the assertEquals method:
assertEquals("photos are not the same", expectedPhoto, actualPhoto);
The pixel class helps you create and use pixels of specific colors.
You have already seen above how you can retrieve a pixel from a photograph.
To create a new pixel with specific RGB values, you can use the statement:
int r = 255, g = 255, b = 255; // a white spot
Pixel p = new Pixel(r, g, b);
Note that the values for the parameters can only be between 0 and 255. The pixel class doesn't actually check that this is true, and if you provide incorrect information the results will be unpredictable.
To view the results of your hard work and/or save it to a file, you can use the PictureUtil class.
To open a window with your photograph you can use the method show:
PictureUtil.show(photo, "Title"); // opens a window with the title "Title" that contains your photograph
PictureUtil.show(photo); // opens an untitled window with your photograph inside
To save a photograph into a file, use the method save:
PictureUtil.save(photo, "myPhoto.jpg"); // saves the object photo into the file myPhoto.jpg
For this project you will have to implement several methods that perform image manipulations similar to the manipulations you might be have used in photo editing software. As already mentioned, we are not providing any starter code for the methods (though a sample method is already implemented) and you will have to create all the required methods from scratch. Please pay close attention to the names and parameters of the methods - even the smallest typo or 'creative license' will result in your code not passing the tests even if the images on your screen look just fine.
You will find the sample method in the file PhotoEdits.java and you will have to create your methods in the same file.
All the methods you implement must check that the photograph provided as input is not null, and return null if it is (see example in the makeBlue method). The special value null must also be returned when the parameters passed to the methods are incorrect.
Make your picture monochrome
Implement three methods named:
These methods take one parameter of type Photograph and return a new Photograph where all but one of the color channels have been set to 0, as indicated by the name. For example, the makeBlue method (already provided to you) takes the blue color from the original photograph but sets the red and green colors to 0.
Dim the picture
Implement a method named dim that takes two parameters - one of type Photograph and one of type double, representing a fraction by which the image must be dimmed. The method returns a new Photograph where each pixel is dimmed by the fraction provided as a parameter.
If the fraction is a negative number or larger than 1.0 the method should simply return the special value null.
To dim the photograph, simply multiply the RGB values of each pixel by the fraction provided as a parameter.
Filter the picture
Implement three methods called
that reduce the intensity of the corresponding color channel. Specifically, these methods accept two parameters - a Photograph and a double, representing a fraction by which the intensity of the corresponding color must be reduced. The methods return a new photograph that is dimmed in only one color channel.
If the fraction is a negative number or larger than 1.0 the method should simply return the special value null.
Implement a method called grayscale that takes a Photograph and returns a new Photograph where the pixels have been replaced with a grayscale value of the intensity of the original pixels. See the discussion above about pixel intensity. More specifically, to successfully implement this method you will need to compute the intensity of each pixel in the original photograph and assign this intensity to all the RGB channels. Pixels with the same value for each of the three primary colors appear gray.
A fun image transformation is posterization - a process that binarizes the color profile of an image. Specifically, for a pixel with color channels R, G, and B, the posterized version of the same pixel will have the corresponding values set to 0 if the original value was less than 128, otherwise the pixel is kept at it's original value.
Implement a method called posterize that performs this transformation. This method must take a single parameter of type Photograph and return a new Photograph that has been posterized.
Flip the image
Implement two methods named:
that flip the input image along a horizontal or vertical axis, respectively. Both methods must accept a single parameter of type Photograph and return a new Photograph where the image was appropriately flipped. For the horizontal flip, for example, the left and right parts of the image are swapped. More specifically, each pixel is replaced with the pixel that is its mirror with respect to a vertical line crossing the middle of the picture. flipVertical does the same but along a horizontal axis going along the middle of the figure.
One challenge with this assignment is making sure you save the pixel that will be overwritten, otherwise its value will be forever lost. We will let you figure this out, but to exemplify the issue you will have, see the following bit of (incorrect) code:
// here we try to swap the values of p1 and p2
Pixel p1 = photo.getPixel(100, 200);
Pixel p2 = photo.getPixel(200, 100);
p1 = p2; // at this point the value of p1 is lost and we don't know what to put in p2
This is the first of two more complex problems that require you to inspect multiple pixels around a pixel of interest. For this first assignment we will try to find edges in the image, defined as regions of the image where the intensity changes abruptly. Edge detection is an important component of machine vision approaches as it allows the computer to separate out the different objects it "sees" in a scene. For the purpose of the project we will implement a simple procedure that attempts to find horizontal and/or vertical edges.
Specifically, assume you are trying to find whether the pixel at position (x, y) is part of an edge. We will define the 'edginess' of this pixel to be the larger of two values - its 'horizontal edginess' and its 'vertical edginess', defined as follows.
The horizontal edginess examines the 6 pixels surrounding our target pixel, marked with Ts and Bs in the figure below:
The horizontal edginess of (x,y) is simply the absolute value of the difference between the intensities of the pixels marked T and the pixels marked B above. See above for the definition of intensity that you need to use here.
A similar definition applies to the 'vertical edginess':
except that the difference will be between the average intensity of the L pixels and the R pixels.
You must implement a method called edge that takes a parameter of type Photograph and returns a new Photograph where each pixel is assigned a grayscale intensity (all RGB values set to the same value) corresponding to the maximum of the horizontal and vertical edginess values for that pixel.
Note that edginess is not defined for the boundaries of the image and these must be set to black.
Blur the image
You must implement a method called blur that takes two parameters, one of type Photograph and the second of type int, and returns a new Photograph where each pixel is the average over all pixels in the original figure that occur within a square of size determined by the second parameters. Specifically, assume you are looking at pixel (x, y) and the parameter was 2, the intensity of the new pixel will be the average intensity of all pixels in the 5x5 grid shown below:
The average is taken separately for each color channel.
Note that you must carefully consider the 'edge' cases - where an image boundary crosses the selected grid. You must make sure you don't try to access pixels outside of the photograph (which would result in an error) and you must also correctly compute the average if less pixels are used in the calculation.
Ungraded fun extensions
Here are some suggestions of fun things you can do:
- Combine different transformation - using a red and green filter, for example, you can get a yellow filter.
- Inverse - 'flip' the individual colors by subtracting them from 255 (bright becomes dark and vice versa)
- Combine two photographs of the same size by keeping just the maximum, minimum, or average of the intensities of the different color channels
- Pixelate - blocks of pixels of size x size get assigned the same color as the pixel in the middle of the grid.
- Rotate colors - red becomes green, green becomes blue, and blue becomes red
- Rotate image - rotate the image clockwise or counter-clockwise 90 degrees
- Offset the image by a certain x and/or y offset - shift the image, making sure you correctly handle edge cases (some pixels will 'fall off' and others will have to be filled in with black color). If you also implemented a combination strategy, you can combine a picture with its offset version to generate fun effects.
- Fake 3D - take a photograph, make two versions of it, coloring one in red and the other one in blue (instead of grayscale assign the corresponding intensity to just one of the channels), offset the two versions from each other, then combine them into a single image. Viewing the picture with 2-colored 3D glasses should make it appear three-dimensional. This may require a bit of fiddling around with both the amount of offset, and the type of photograph you use. My guess is that a single well-defined object on a monotone background may work best.
While most of the manipulations require a doubly-nested for loop (the standard strategy for accessing grids), the edge detection and blur effects will require additional inner loops to aggregate the information from multiple neighboring pixels.