Due: Friday, April 28th at 11:59pm
Digital Colors are traditionally represented with 3 components: red, green, and blue. If you were to hold a magnifying glass up to your laptop's screen, you would likely see each dot, or "pixel", is actually 3 smaller dots whose colors are red, green, and blue.
By varying the intensity of these red, green, and blue dots, from 0% to 100%, your display can output the entire range of colors you see on your screen. All three at 0% is black, all three at 50% is the mid-range gray, all three at 100% is white. Carolina Blue, for example, is 48.2% red, 68.6% green, and 83.1% blue.
In our code, we will represent these percentages with double values between 0.0 and 1.0.
Digital Images, then, are 2D arrays of Colors. The size of the 2D array depends on the quality of the picture. A modern smart phone, like the one in your pocket, captures pictures with anywhere between 4 to 16 million pixels. (Hence "megapixels".) We'll be writing an Image class that lets us store and manipulate hundreds of thousands to millions of Color elements in a 2D array in Java!
Digital photo Filters are algorithms that receive an input Image, process each Color "pixel" in it element-by-element, and return an output Image that has been processed.
In this assignment we will implement a few Filters each with one specific purpose that can be applied in a varying amount. For example, a Border Filter's goal is to draw a colored border around the Image and the border's thickness is a variable amount. Another example is a Brightness Filter whose goal is to brighten or darken an image by some amount.
Errors Warning: You will see red X errors in your project coming from the comp110.filters package until you reach Part 3. This is expected. You can ignore these! If Eclipse pops something up each time you run telling you there's an error. Check "Always launch without asking" so that you are not bothered with this each run in Parts 1 and 2.
To get started, follow these steps to install the support code:
The Color class will have three primary responsibilities.
Your work in this section will go in the comp110.Color class. We have provided an empty ColorTests class in which you can and should test your methods and constructor as you add them. This is really good practice with early semester concepts. The TAs will expect you have test code running for any requirement you need help with.
Declare your private Color fields. You will need three, one for each of the components of a Color: red, green, and blue.
Fields: _red, _green, _blue Each have the following properties: Type: double Visibility: private
Each of your Color's three components (red, green, blue) must have public getter methods.
3x Getter Methods Name: get<component> where component is Red, Green, Blue Visibility: public Parameters: none Return Type: double
Each getter must return its corresponding field's value.
1.3 Define Setters for the Components
Each component (red, green, blue) must have public setter methods defined, as well.
3x Setter Methods Name: set<component> where component is Red, Green, Blue Visibility: public Parameter: double value Returns: void
Each setter must assign the value provided as a parameter to its corresponding component.
Component values must remain between 0.0 and 1.0 as these represent the percent magnitude of each component. There's no such thing as 200% red or -5% blue.
Why enforce these bounds here? Later we'll write a Filter like brightness. Imagine a photo of the sky with clouds that are pure white (100% red, green blue) and sky that is blue. The lighten filter will increase each Color's component by some percentage. How do you brighten pure white? You can't! Rather than worrying about going out-of-range in each filter, we'll simply enforce the range here.
Your task: when a setter method's value parameter is not within the range of 0.0 to 1.0, you must ensure that the value assigned to the field is in that range. For values less than 0.0, assign 0.0. For values greater than 1.0, assign 1.0.
Challenge: try to do this with only a single if-then-else statement. How? Define your own helper method!
Add a Constructor to the Color class with the following parameters:
Constructor Visibility: public Parameters: 1) Type: double Name: red 2) Type: double Name: green 3) Type: double Name: blue
The constructor should assign each of the parameters to its corresponding field. It should also ensure that each value stays within a 0.0 to 1.0 range. Hint: you can call methods on the same instance from within the constructor. Try making use of your setter methods from part 1.3 rather than assigning directly.
Note that once you've changed your constructor's parameters, you'll need to update where you construct it in your tests file to match.
Add a method with the following characteristics:
Name: copy Visibility: public Parameters: none Return Type: Color
When this method is called on an instance of the Color class, it will return a new instance of the Color class whose component values are the same as the original. In that sense, is a lot like the clone method from ArrayUtils, but with an object instead of an array.
The code to achieve this is simpler than it sounds and should take a line or two. First, construct a new instance of the Color class using your fields as arguments, then return that instance.
How can you test this? Once you have a copy, you should be able to set its component values without the original's changing. This is exactly what we'll do when we apply filters!
Open ColorGUI.java and at the TODO labeled 1.6, rather than assigning null to the sample Color, create a new instance of the Color class and assign it instead. Try running and you should see the color you constructed appear in the GUI. You're ready for Part 2!
Curious about the code behind this GUI? I hoped so! There's not much. You can see it all in the Package Explorer by expanding Referenced Libraries > ps05-support.jar > comp110.support.
Video walk-through introducing 2D array concepts.
Your Image class will store a 2D array of Color objects. Each of these Color elements is called a "pixel" in this context (short for "picture element"). Your Image will have methods to get and set of pixel Colors based on (x,y) coordinates.
Pixels in your image will use an (x,y) coordinate system just like the Emoji project. Coordinate (0,0) is not at the center of your image! Coordinate (0,0) corresponds to the top left of your image, (width-1, 0) corresponds to the top right, (0, height-1) to the bottom left, (width-1, height-1) to the bottom right. Great news, though, this coordinate system maps 1:1 with the indices of your 2D Color array. (Which, as it turns out, is why this coordinate system is common to most 2D graphics applications (window layouts, html/css, graphic design, etc).
An Image's only field is its 2D array of Color instances we call "pixels". Declare a field with the following characteristics:
Name: _pixels Type: Color Visibility: private
Your Image class needs a constructor with the following characteristics:
Visibility: public Parameters: 1) Type: int Name: width 2) Type: int Name: height
Initialize your _pixels array to be a new 2D array of colors whose first dimension's size is the width parameter's value and whose second dimension's size is height's.
Once initialized, each pixel element's value defaults to null. Next you'll need to initialize each element in the array to new instances of the Color object. A new Image should be a blank canvas, so make sure each element is white (100% red, 100% green, 100% blue).
To do this, you'll need to write a nested for-loop to iterate through every element in the private pixels array and assign each element a new Color instance.
You should now be able to construct a new instance of the Image class from the ImageTests file. How will you know if your constructor is actually working? You could try adding some println statements in your constructor, or you could try using the debugger as shown in Lecture 24. The next sections will also give you an opportunity to test.
Why no setters, too? For our purposes, the width and height of an Image are read-only after construction, so you only need getters for each.
We don't have fields to store height and width because you can get that information directly from the _pixel array's length properties. Hint: _pixels's type is a Color array and, as such, has a length property: _pixels.length.
- Name: getWidth Parameters: none Visibility: public Return Type: int - Name: getHeight Parameters: none Visibility: public Return Type: int
Test these methods in your ImageTests file by printing out the width and height of an Image instance and confirm each matches the dimensions you constructed the Image with.
An image filter needs to be able to process pixels in our Image. It will do so by asking an Image for a pixel at a specific coordinate via a getter method and replacing the pixel with a modified version via a setter method.
Define these methods with the following characteristics:
- Name: getPixel Parameters: 1) Name: x Type: int 2) Name: y Type: int Visibility: public Return Type: Color - Name: setPixel Parameters: 1) Name: x Type: int 2) Name: y Type: int 3) Name: color Type: Color Visibility: public Return Type: void
The getter should return the Color object stored at index _pixels[x][y].
The setter should assign the color parameter to the _pixels array at coordinate [x][y].
Try thinking of a way to test these methods from the ImageTests class.
Just like we needed to be able to copy a Color, we need to be able to copy an Image (which will in turn copy all of its Color elements). This way when we apply a filter, we will not overwrite the original Image data. Your copy method should have the following characteristics:
- Name: copy Parameters: none Visibility: public Return Type: Image
When this method is called on an instance of the Image class, it will return a new instance of the Image class that is a complete copy of the instance it was called on.
Within the copy method you must construct a new instance of the Image class whose width and height are the same as the Image copy was called on (hint: this Image). Then you will need to iterate through every pixel, copy that pixel, and assign it to the same x, y coordinate in the new Image. Finally, return the copy.
You can test this in Image tests by making a copy of an image, changing one via setPixel, and ensuring that the original is left unmodified. This will also be thoroughly put to use in part 3: filters.
If all is well, you should see a picture of the Old Well appear and no errors printed to console! After this, you're ready for the real fun: writing filter algorithms!
In Part 3 of this assignment, you will implement filters that manipulate Image and Color data. The user interface for interacting with your filters is provided in support code. From it you can:
1) Load a different image than the default Old Well image.
2) Select the filter you are applying to the image.
3) Manipulate the amount the filter is applied from 0.0 to 1.0. This value will be given to your Filter. Our code will then run your Filter and display the results.
4) Save your Image after your Filter has been applied for Twitter/Instagram glory.
Your work on filters will be done on classes in the comp110.filters package. Begin by opening up the Filter interface. It declares every Filter will have these four methods:
1. toString - This method will return the "name" of a Filter.
2. getAmount / 3. setAmount - The "amount" property of a Filter controls the intensity in which the filter is applied to the original image.
4. process - Each filter's algorithm is implemented in the process method. It will take an Image as an input, copy the image, apply the filter to each Color in the Image, and then return the filtered Image.
Begin by running CompstagramGUI to see the user interface. If you have errors at this point, it's likely due to lingering issues from Parts 1 and 2.
No filters are on the filters list! Let's fix that. Open the file comp110.filters.CompstagramModel (notice this is in the comp110.filters package!) Find the comment on 3.2 Invert and uncomment the line below. This line is adding a new InvertFilter to the list of filters
The class InvertFilter is being given to you in its entirety with narrative code comments. This is meant to serve as an example for how a Filter class is structured and how a processing algorithm is implemented. You should read InvertFilter.java for understanding and tinker around with its amount slider in the GUI.
The first Filter for you to implement is BorderFilter. To get started, in CompstagramModel, find the Part 3.3 comment and uncomment the line below where the new BorderFilter is being constructed. Try running and you should see "Border" show up in your Filter drop-down now. If you try changing the "Amount" slider, nothing happens! Your work begins...
Go ahead and open up BorderFilter.java. The goal of BorderFilter is to add a border around your input Image whose borderthickness is controlled using the _amount field. The app's Amount slider will set this value between 0.0 (meaning no border) and 1.0 (meaning entirely border). Here are some examples:
You'll use the following formula for calculating the border's width:
borderthickness = imagewidth* filteramount / 2
You'll need to setup a local variable within the process method to hold borderthickness. Its type will need to be an integer, because the array indices you will reference are integers. However, the _amount field is a double. You will need to cast your final result back to an integer, which will look something like this:
int thickness = (int) ( <expression> );
Spend a minute to reason through why you're using this formula. Why are you dividing the image's width in 2? What is the impact of multiplying half of the width by a value between 0.0 and 1.0?
In the same way that InvertFilter's process method works, you'll need to:
1. Create a local variable to hold a copy of the input Image
2. Write a nested for loop to iterate through and process every pixel
3. Return the copy Image
Now that you know the border's thickness and you're iterating through every pixel in the Image, how will you know whether any individual pixel is a part of the border or not? Using the x, y coordinates of a pixel, if any of the following four inequalities are true, you'll know you are in a border area:
|Left||x < borderthickness|
|Right||x >= imagewidth - borderthickness|
|Top||y < borderthickness|
|Bottom||y >= imageheight - borderthickness|
If you are in a border area, the Color you will set each border pixel to is held in this BorderFilter's field named _color.
It's a good idea to start with trying to draw one of the border sides first and then add the others one-by-one. Drawing a border will put your knowledge of if-else-if statements and boolean logic to good use!
Let's make it possible to add lightness or darkness to your Images!
Now that you've been around the block with the Border filter, you'll take on modifying pixels by their red, green, and blue component values. Open up BrightnessFilter.java. Notice it has a lot of similarities with BorderFilter.java. Again, you're tasked with implementing the process method's algorithm.
For Brightness, and the filters that follow, we will not walk through the steps to copy the input image, process it via for loops, and return the processed image. Refer to those steps from BorderFilter or InvertFilter.
With BrightnessFilter, you want an amount of 0.5 to result in no change in brightness to your image, 0.0 to be 100% darker than the input, and 1.0 to be 100% lighter than input. Remember, as each red/green/blue component of a Color decreases toward 0.0 it becomes darker. As each increases toward 1.0 it becomes lighter.
As such, here is a formula for calculating our brightness factor.
factorbrightness = (filteramount - 0.5) * 2.0
Take a minute to think about what your brightness factor will be if your filter's amount is any of 0.0, 0.5, and 1.0. Essentially, you're "translating" amount to between -0.5 and 0.5 and then "scaling" it by 2.0 so that your possible factor domain is between -1.0 and 1.0.
Now that you have that stored in a local variable, you need to manipulate each of pixel's three components with the following formula:
outputcomponent = inputcomponent + ( factorbrightness * inputcomponent )
To modify each pixel's components, you'll first need to get the Color pixel from your copy image, then get each of its component values, manipulate them, and store the manipulated values back in the Color pixel with setters. For an example of working with components like this, refer to InvertFilter's process method.
Let's make it possible to bring your Image's Colors closer and closer to Carolina Blue (or any other Color)!
Phew, two Filters under your belt. Nice work! Now that you've got Border and Brightness down, you're going to write an algorithm to bring an input Color closer and closer to being another Color. You'll do this for every Color in your Image.
If you think about this in terms of an input Color of white, with RGB components of (1,1,1), and a target Color of black, with RGB components of (0,0,0), you can say you've brought our input 50% closer to your target if its new RGB components are (0.5, 0.5, 0.5). In essence, you've subtracted half of the "distance" between your input color and your target color.
ColorizeFilter's amount field will control the delta, or the percent of the "distance" you'll move each Color from its input toward its target (i.e. Carolina Blue). So an amount of 0.0 will be no change, 0.5 is 50% closer to Carolina Blue, and 1.0 is turning the each Color completely Carolina Blue. Here's how you can calculate the delta for any component.
deltacomponent = ( inputcomponent - targetcomponent ) * filteramount
Your target Color is held in this ColorizeFilter's color field, just like with BorderFilter.
Now that you have your delta for each component, subtract it from the respective input component and apply it to the pixel you're processing, as below.
outputcomponent = inputcomponent - deltacomponent
The InvertFilter goes through a very similar process and may help illustrate what's going on. In InvertFilter, though, each pixel's target color is that color inverted. Here it's the Color stored in the color field.
Contrast is the difference in color that makes features of an image perceptible. This is a hard to explain in with words but the filter is easy to see when looking at example pictures or by playing with contrast of a photo on your phone.
In essence, as you reduce the contrast of an Image, you are pulling each of its Colors closer and closer to 50% gray (0.5, 0.5, 0.5). It turns out you've already done this with our Colorize filter! There's an extra wrinkle, though: how do you increase contrast? What does it even mean to increase contrast? If you think of increasing as the opposite of decreasing it would follow you're pushing a Color further and further away from 50% gray.
You'll achieve increasing and decreasing contrast by combining the techniques you applied separately in brightness and colorize.
In BrightnessFilter, you apply some arithmetic to amount to come up with a factor whose domain is between -1.0 and 1.0. With Contrast we'll do something similar except flip the signs by subtracting from 0.5. The rationale for this is as you decrease contrast you want to move closer to gray.
factorcontrast = (0.5 - filteramount) * 2.0
In ColorizeFilter, you have an algorithm for bringing each Color in an Image some amount closer to a target color. In ContrastFilter you will need to use factor instead of amount when finding the delta. Spend a minute reasoning through what impact this should have on varying component values.
deltacomponent = ( inputcomponent - targetcomponent ) * factor
Otherwise, your contrast algorithm is the same as colorize's assuming your target color is 50% gray or new Color(0.5, 0.5, 0.5).
Your last required filter! Good news: it's really easy after doing contrast! Our SaturationFilter is the exact same as ContrastFilter, with one minor tweak: your target Color is different for each pixel. Let's back up.
You can think of Saturation as the intensity of non-gray Colors in an Image. A completely desaturated Image can be thought of as a grayscale or "black and white" Image. Black and white photos are actually many shades of gray. An interesting property of pure black, grays, and white is that each of their three component values are exactly the same. White is (1, 1, 1), grays vary from (0.99.., 0.99.., 0.99..) to (0.5, 0.5, 0.5) to (0.01,0.01,0.01) and black is (0,0,0). Grayscale implies the red, green, and blue components of each Color are equal to one another.
How can you convert any Color to be grayscale? Spend a minute to think about this.
It turns out there are many ways to convert a non-gray Color to grayscale. (You can read a lot on the internet about it!) For this assignment, you'll just use a simple hack: take the average of each component and use that.
componentaverage = ( componentred+ componentgreen + componentblue ) / 3.0
You'll construct a new Color using that average value for each of its red, green, and blue components. This will be your target grayscale Color.
Each pixel you process, then, has a different target Color. Barring this difference, the algorithm to implement in SaturationFilter is exactly the same as ContrastFilter. We'll leave this one to you to modify the work you did in Contrast!
Be sure to fill in the standard header for each of Color.java, Image.java, BorderFilter.java, BrightnessFilter.java, ColorizeFilter.java, ContrastFilter.java, and SaturationFilter.java.
How are actual Instagram filters made? Instagram Filters combine a lot of simpler filters like the ones you implemented above. Try writing your own "CompositeFilter" implementation that has a List of Filters it uses to process your image. It should feed the output of one filter into the input of the next until it has run through each filter in its list. Can you think of a way to do this?
Other ideas follow. Try searching the internet for ideas on how to achieve these. If you implement your own custom filters, come show us or e-mail your team a resulting photo!
- Box Blur
- Gaussian Blur
- Linear Gradient
- Edge Detection
- Image Overlay