CS 161 Lab F - Photoshopper
Due Fri Oct 18 at 9:00am
Overview
In this lab, you will be complete a program that lets you manipulate a graphical image, similar to the ever-popular Photoshop application. Your program will open an image file, display the image, and allow the user to apply a number of effects to that image. The basic functions you will be implementing are:
- Erasing (clearing) the image
- Turning the image into gray scale (i.e., making it black and white)
- Turning the image upside-down (i.e., flipping over the central horizontal axis)
- Turning the image into its "negative"
- Being able to "undo" the previous effect
Although this may sound complicated, I'll walk you through the steps needed. Make sure you read the lab directions carefully, think about the directions, and plan before you code.
When running, your program will look something like this (all examples are made using the eye.jpg file included in the project file):
I have provided the code to create the window with the buttons for you (we'll talk about how to do this yourself later!)
This lab will be completed in pairs. Be sure to review the pair programming guidelines. You also must work with a different partner than you have before!
Objectives
- To practice with loops (nested loops in particular)
- To experiment with image manipulation and processing
- To practice with the BufferedImage class and to continue exploring how to manipulate Colors.
Necessary Files
You will need to download and extract the BlueJ project from the LabF.zip file. This project will supply you with the following classes:
-
ImagePanel
This handles the work of actually displaying your image and takes care of the details of sizing it for the window, etc. You do not need to modify this file, nor will you use it directly. -
PictureViewer
This opens a window, shows the image and provides buttons for the user to click to manipulate the image. You do not need to modify this file, nor will your code use it directly. -
PictureViewerTester
This tester class can be used to run the program. You can also test the program by simply creating a new PictueViewer object using the BlueJ interface (which is exactly what the tester does!)
The lab file also has a couple of images you can use to test your program, though the system will be able to work with any file with a .jpg extension (called a "jay-peg" file). Feel free to use your own pictures if you wish (but try and avoid using overly large pictures; it will slow down your program!)
Assignment Details
For this lab, you will be writing a new class called PhotoShopper
(make sure to get the capitalization correct!) This class will do the actual image manipulation of your program, such as flipping the image, making it gray-scale, etc. When your class is finished, you will be able to construct (right-click in the BlueJ window) a PictureViewer. When the PictureViewer starts, it will look like this:
Each of the buttons in the PictureViewer window will be associated with a method in your PhotoShopper
---in effect, clicking a button will call a method you write to manipulate an image.
-
Note that the "Load Image" button is already implemented for you: it opens up a dialog box (called a JFileChooser) that will let you select an image to manipulate. The user can navigate to find the image and select it. Once the image is selected, the PhotoViewer loads it from the file and creates an new
BufferedImage
object to represent it. ThisBufferedImage
is then passed as a parameter to the constructor of your PhotoShopper.- What does the constructor for the PhotoShopper need to look like?
- When the user clicks other buttons, the PhotoViewer calls methods in your PhotoShopper. Each of those methods manipulates the BufferedImage object and returns a new, changed BufferedImage object as the return value. Finally, when the user clicks the "Save Image" button, the PhotoViewer retrieves the image from your PhotoShopper and saves it to the harddrive, so you can share it with your friends! (This "save" functionality has also already been implemented for you)
-
All of the button interaction is provided--you just need to fill in the appropriate methods in the PhotoShopper class. These methods are described in more detail below.
- This program should also serve as an example of good Object-Oriented Programming--we're defining a PhotoShopper object that can be used independently of the PictureViewer class! Can you see how this structure has let us break up our code, making it easier to write and test new functionality?
Colors and BufferedImages
Your PhotoShopper class will need to have an attribute that is of the type
BufferedImage
to represent the current image that is being modified. Be sure to look over the documentation for this class. Some details are below:
A
BufferedImage
is an object that represents a graphical image. Each image consists of a number of colored points, called 'pixels'. Each pixel has an x and y coordinate and a color. BufferedImages have lots of available methods, but for this lab we are mostly interested in finding out what the color of a pixel is, and changing the colors of the pixels.Each pixel's color is represented by an integer. This integer is calculated based on three components (or channels): a red component, a green component and a blue component. Each component is an integer with a value from 0 to 255. If all components are 0, the color ends up being black. If all components are 255, the color is white. Other combinations give other colors. You can see what colors different combinations lead to here. This method of specifying colors (there are other methods too) is called 'RGB' for "red green blue".
To modify the pixel of an image, you need to get all of the RGB components, change them individually, and then set the appropriate pixel in your new image (see below) to that new color. However, it is not immediately obvious how to access the the individual components of an integer. One reasonable solution is to take that integer and use it to create a
Color
object, such that we've worked with before.Color
objects have getters (e.g., getRed()) and setters that we can use for each individual component, as well as for the entire RGB integer (getRGB()). It also has a constructor that lets us specify the 3 channels as integers, making it easy to move back and forth between representing colors asint
s andColor
objects.
- Take a moment and think about this idea of "representation." It's the same way we talked about using either a
double
or anint
to represent a number; now we're using either anint
or aColor
to represent a color!So the basic process is: get the RGB
int
and then make aColor
object out of it. From theColor
object, you can get the red, green and blue components as separateint
s. After manipulating the red, green and blue components, make a newColor
object from them, and then use theColor
object to give you the appropriate rgbint
for the pixel, and put thatint
back into theBufferedImage
(whew!). Here is the process in code:int oldRGB = oldImage.getRGB(x,y); //get the RGB for the current pixel Color oldColor = new Color(theRGB); //make a color so we can extract RGB components // get the components int red = oldColor.getRed(); ... //etc. // process the color components here Color newColor = new Color(newRed, newRreen, newBlue); //make the new Color object int newRGB = newColor.getRGB(); //get the RGB for it as an int newImage.setRGB(x, y, newRGB); //set the pixel in the new image to be that color
Creating the PhotoShopper class
-
Your PhotoShopper will need to include two import statements to get access to the BufferedImage class and the Color class:
import java.awt.image.BufferedImage; import java.awt.Color;
-
Your PhotoShopper will need to have a constructor and a
BufferedImage
instance variable to represent the current image. -
The PictureViewer won't compile until you have defined all the PhotoShopper methods. But you don't want to write everything before you test (that would be bad!). It is much easier to write and test one method at a time. So start by making a "skeleton" of the class: fill in the method signatures for all of the methods you will need (there are 8 including the constructor). These are called "stubs" by programmers. For example, here is a stub for the
eraseImage
method:public BufferedImage eraseImage() { return null; //we return null so that we're returning SOMETHING, allowing the code to compile. }
Once you have written the stubs, you can compile the PhotoViewer, load an image, and then begin programming by adding and testing one method at a time.
- Note that every method you write will have a similar method signature: they should take no parameters, but return a
BufferedImage
object. The PictureViewer will be saying "hey, please give me a copy of the image (with some modification) so that I can show it", and the PhotoShopper will be reponding with a new BufferedImage to show.
- Note that every method you write will have a similar method signature: they should take no parameters, but return a
- Don't forget to include a comment for each method!
Modifying the Image
Each of the methods you will write manipulates the BufferedImage object by changing the pixels that make up the image. It then returns a new manipulated BufferedImage. Write one method at a time and test it. Once you have the first one done, the others will be much easier because most follow the same basic outline. The basic outline for each of these methods is this:
(1) get the dimensions of the current image (2) make a new BufferedImage of the same size (3) use a pair of nested for-loops to process each pixel, assigning the new color to your new BufferedImage (4) save the new image in place of the old one (5) return the new BufferedImage that you created
These steps are detailed below:
-
Because you might be modifying different sized images, your methods will need to first get the width and height of the current image, so that you know how many pixels to look through. You can access this information by using the
getWidth()
andgetHeight()
methods of theBufferedImage
class. Note that these are getters, just like every getter you've ever written or used! -
Next, you'll need to create a new
BufferedImage
to hold your modified image. We can't modify the image in place, because we might end up losing information. This new BufferedImage should be the same size as your previous one. Remember that the constructor for a BufferedImage looks like:BufferedImage newImage = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
-
You'll need to use a pair of nested for-loops to process each pixel in your old image, in order to determine what corresponding (modified) pixel color to place in your new image.
- Consider using "x" and "y" as the variables for your for loops--this will let you easily think about iterating over all of the (x,y) coordinate combinations!
- Details on how you should modify pixels can be found in the below description of individual methods. Note that you will need to use the above conversion process to move between
int
andColor
representations of colors. - NOTE: You want want to get the colors from the pixel in the old image, and use those to set the colors in the new image! Do not modify your old image at this point.
- If you have any questions about this, ASK!
- After you've finished making the new image, you'll want to "save" that image inside your instance variable. You can do this by just assigning the new image to that instance variable!
- Finally, you'll need to return the new image (which should also be your instance variable at this point.
You can use nested for loops to process each pixel by making a for loop that iterates through each x-value. Within that loop, use another for loop to iterate through each y-value.
eraseImage()
This should be the first method you write. Note that it should have a method signature as described above.
This method should set all of the RGB components to 255 (making the pixel white). This is a great method to write first, since all of the other methods will follow this pattern. After writing this method, hitting the appropriate button should produce an image like:
grayScaleImage()
This method should take each pixel from the original image and figure out the average of the red, green, and blue components. Use that average for all of the red, green, and blue components of the corresponding pixel in the new image. After writing this method, hitting the appropriate button should produce an image like:
flipImage()
This method flips the image upside down!
This method should take each pixel from the original image and put it in the corresponding place in the new image by giving it a new y-coordinate. Think about where each pixel needs to go in order to flip the image upside down--what math will you need to perform? After writing this method, hitting the appropriate button should produce an image like:
negativeImage()
This method should take the red component and subtract it from 255 to get the new red component value. Do the same for the green and blue components. After writing this method, hitting the appropriate button should produce an image like:
undo()
Note that this method will require modifying all of the previous methods!
You can implement a simple "Undo" method by saving a copy of the image before changing it. Make a new instance variable (e.g., oldImage), and assign the current image to that variable at the start of each method before you make any changes. Initialize this instance variable as null
in your constructor.
Then inside your undo() method, simply assign the oldImage instance variable to the current image instance variable, and then return the now current image.
getImage()
Save simplest for last! With this method, you just need to return the image (no need to process each pixel!) This method is used by the "Save image" button, and will let your program save the modified image to a file.
You will also need to include the signature for the blurImage
extension so that your code compiles (though completing this method is optional).
Submitting
Make sure both your names are on the PhotoShopper class, and upload the entire project directory to the LabF submission folder on hedwig. Only one partner needs to upload the code. Make sure you upload your work to the correct folder! The lab is due at the start of class the morning after lab.
After you have submitted your solution, log onto Moodle and submit the Lab F Partner Evaluation. Both partners need to submit evaluations!
Extensions
As an extension, you might try adding functionality for the blurImage()
button. Details below:
Optional:
blurImage()
- This method is the hardest to write. What you need to do is for each pixel, set each color component (red, green, and blue) to the average of the appropriate color component of ALL PIXELS nearby it. This ends up making each pixel look more like its neighbors, and has the effect of blurring the image.
-
Each pixel has 8 neighbors next to it (north, northeast, east, southeast, south, southwest, west, and northwest). However, just averaging the immediate neighbors won't produce a huge effect. You'll get a much more noticable blur if you instead average all the pixels within a certain distance of your "center" pixel (4 is a good distance for us, but you can use a smaller distance if your program seems to be running slowly). You can do this by using another pair of for loops:
for(int i=row-distance; i<row+distance; i++) { for(int j=row-distance; j<row+distance; j++) { //add the value of each color component to running total } } //divide the running totals by the number of pixels you looked at to get the averages
Yes, this means you will have 4 nested loops. But don't panic--as long as you're careful, you can keep track of everything!
- There is one more trick though: pixels near the borders don't have that many neighbors to each side: for example, the pixel at (0,0) can't get his neighbor that is 4 pixels to the left, since that pixel would be at (-4, 0). So you will need to make sure your loops do not go outside the bounds of the image. Your
i
cannot be less than 0 or greater than (or equal to) the image height, and yourj
cannot be less than 0 or greater than (or equal to) the image width. You should use theMath.min()
andMath.max()
functions to make sure that your starting value (e.g., row-distance) and your stopping value (e.g., row+distance) are inside the legal bounds! - After writing this method, hitting the appropriate button should produce an image like:
Grading
This assignment will be graded on approximately the following criteria:
- Constructor, attributes, and getImage() [10%]
- Erase (clear) the image [15%]
- Turn the image into gray scale version (i.e. black-and-white) [15%]
- Flip the image (i.e. mirror over the central horizontal axis) [15%]
- Turn the image into its negative [15%]
- Undo function [15%]
- Use of good programming style [10%]
- You completed your lab partner evaluation [5%]
- Blur the image [+10% extra credit!]