Homework 6: Pennstagram

In this homework, you’ll be completing a basic photo manipulation application in Java!
Task 0: Set up your project and browse the files
Setup (Codio Users)
You do not have to download the files for the homework assignment – the Codio box has been configured with them for you.
Setup (IntelliJ Users)
Download IntelliJ, following the instructions in our IntelliJ set-up guide.
Once it’s downloaded, set up your Java project, following the instructions in the “Setting up homework in IntelliJ” section of the guide linked above. Make sure not to put any special characters (other than hyphens and underscores) or spaces in the full absolute path of the project.
You can download all the code files here, and import them into a Java project. If you’re not sure how to run files, tests, the style checker, etc., please check the setup guide linked above.
Project Overview
We’ve given you the GUI for this program and have defined several super-fancy photo effects, controlled by buttons on the right side of the application (“1890s”, “Pin Hole”, etc.). However, these effects don’t quite work yet, because they depend on basic photo manipulation algorithms that you must implement. (These basic manipulations are controlled by the buttons on the left side, provided for your testing convenience.)
We’ve given you a few completed files to get started:
PixelPicture.java
, which manages the reading and writing of image data.GUI.java
, the simple GUI for the program (which you can run to help you test Parts 3 and 4).ColorMap.java
, a Map data structure that helps build histograms.ManipulateTest.java
, a JUnit test file for the image manipulations.PointQueue.java
, a data structure for managing queues of ints (needed only for the Kudos flood fill problem).
And you’ll have to finish off a few more:
Pixel.java
, which is a point of color in an image. You’ll have to finish a few constructors (Pixel()
),getRed()
,getGreen()
,getBlue()
,getComponents()
,distance()
,toString()
andequals()
.MyPixelTest.java
, which contains an example test forPixel
. You will need to add your own tests to ensure your code works the way you want it to.SimpleManipulations.java
, a collection of simpler image manipulations. You’ll have to finishrotateCCW()
,border()
,invertColors()
,grayScaleAverage()
,scaleColors()
, andalphaBlend()
.AdvancedManipulations.java
, a collection of image manipulations requiring pre-processing of the image or consideration of multiple pixels at once in order to paint a single pixel. You’ll have to finishadjustContrast()
,reducePalette()
, andblur()
(andflood()
for Kudos!).Effects.java
, which implements super-fancy photo effects, based on the basic image manipulations (modified only for Kudos).ImageTest.java
, a place to put your own JUnit tests.
The JavaDocs for the classes you are working with can be found here, and the FAQ for this assignment can be found here.
PixelPicture
, GUI
, PointQueue
, ColorMap
or PictureTest
), and
do not add any new files. You’ll be submitting a ZIP archive containing just
the files (listed above) that we are asking you to modify.
Task 1: Pixels
Your first task is to complete the Pixel
class. A pixel represents a color and
is composed of three ints, indicating amounts of red, green, and blue, with
values ranging from 0 to 255. Lower values mean less color; higher values mean
more.
To start off, you will need to think of how to store the red, green, and blue
values associated with each Pixel object. Keep in mind that every new Pixel
created will need to store its own red, green, and blue values. In addition, we
want to make Pixels immutable. That is, once we create a new Pixel
, there
should be no way to modify its RGB values.
Once you decide how to store the different values, complete the two different
constructors. In order to create a new Pixel, one of its constructors must be
called: new Pixel(255,255,255)
represents white, new Pixel(0,0,0)
is black,
and new Pixel(0,255,0)
represents green. If a
is an array containing the
values {0, 0, 255}
, then new Pixel(a)
constructs a blue pixel.
Pixel
class should maintain the invariant that the
three color components are in the range 0 to 255. If a Pixel
constructor is
passed values outside of this range, they should be clipped: negative numbers
get clipped to 0; numbers greater than 255 should be clipped to 255.
After finishing the constructors, complete the following methods:
public int getRed()
public int getGreen()
public int getBlue()
public int[] getComponents()
public int distance(Pixel px)
public String toString()
public boolean equals(Pixel other)
Make sure that your implementation is fully encapsulated, in the sense that it
is not possible to modify the internal representation of an object except by
calling methods from its class. For example, if a client modifies an array
obtained from getComponents
, the Pixel
value should not change. There are
multiple ways to achieve encapsulation, and you do not need to use
Arrays.copyOf()
.
Pixel Testing
In MyPixelTest.java
, write unit test cases for the methods of the Pixel
class.
Pixel
is
finished.
Pictures
This homework will require you to work with bitmaps — two-dimensional arrays of color values — which are a standard representation for images.
Java offers a variety of classes for working with a wide variety of different
image formats. To simplify your life, we’ve wrapped up the tricky bits
of this code in a class called PixelPicture
. The PixelPicture
(and Pixel
)
classes provide all of the basic image management you’ll need. Instead of
working with Java’s image processing libraries directly, you’ll process the
bitmaps provided by this class.
The PixelPicture
class makes image data available to you via the getBitmap
method, which returns a bitmap of Pixels corresponding to the PixelPicture
’s
contents. Note that, in this application, bitmaps are indexed from top to bottom
and from left to right.
| ||||||||
Left-to-right, top-to-bottom pixel layout for a 4 x 3 bitmap |
In the figure above, each dashed box represents an array. The top-level array holds each of the rows of the image, in top-to-bottom order. Each row array holds the pixels of that row, in left-to-right order.
This layout is convenient because it puts the origin in the top-left corner and
lets us visualize the 2-d array as (x,y)
coordinates.
Note that since we index first by row, we access the array first by its y
coordinate and secondarily by its x
coordinate. This means that a coordinate
at position (x,y)
will appear in the array at index bmp[y][x]
. This is
called row-major
layout.
Most tasks in this assignment involve taking a PixelPicture p
, getting its
bitmap via p.getBitmap()
, manipulating the Pixels in that bitmap in some way,
and finally constructing a new PixelPicture
from the manipulated bitmap
using the appropriate PixelPicture
constructor.
p.getBitmap()
gives you a copy of the entire 2-d array. You want to make sure that you
do this only once for each image transformation. If you call p.getBitmap()
more frequently, such as for every pixel in an image during some
pixel-by-pixel transformation, your program will run very, very
slowly.
Implementing the Photo Manipulations
For the rest of the assignment, you will use bitmaps and Pixels to implement some photo manipulation algorithms. These algorithms are described in detail in the sections below.
Make sure to test your functions in ImageTest.java
. If you are using Codio,
follow the instructions found there to run ImageTest
and the provided GUI
through Codio. If you are using IntelliJ, you can run the Gui or the tests by
right-clicking on the file and using the “Run As…” menu item. Either way, the
only buttons that will work initially are “Load new image”, “Save image”,
“Undo”, “Quit”, and “RotateCW”. Note that the GUI downloads its initial image
from the internet, so make sure that you are connected to the internet when
you run it.
AdvancedManipulations.java
, can
be implemented in a very inefficient manner. We have timeouts in place
that will fail individual tests if they take too long. If many of your tests
are taking too long, we will not be able to accept your submission
(because we can’t test it). None of the algorithms we ask you to implement
should take more than a second or two to run if they are implemented
properly.
Effects.java
demonstrates how the basic manipulations
can be used and put together to form composite effects. Reading this file will
help you understand how to use the static methods in SimpleManipulations.java
.
NOTE: For a lot of the assignment, you will be updating individual pixel values. Many (but not all) of the functions require you to divide or multiply by doubles. Since the red, green, and blue attributes of Pixels are ints, you’ll need to do a little work to make sure they are updated with the correct values to pass our tests.
Whenever you need to use doubles in calculations: at the end of your
calculations, you must round using Math.round()
then cast to int. Like
this:
double d = ... /* compute a double */
int val = (int) Math.round(d); /* convert it to an int */
Writing Tests
You can use the files MyPixelTest.java
, ManipulateTest.java
, and
ImageTest.java
to test your code (only MyPixelTest.java
will count
towards your grade). ManipulateTest.java
contains a number of simple tests
for the basic manipulations, and ImageTest.java
uses all of the sample
images from this page as the basis of its tests. Neither of these files is
sufficient to fully exercise the functionality we’re asking you to build, so
you should also create additional test cases to help understand, debug, and
evaluate the code you write.
For help on how to write JUnit tests for this assignment, look at the
provided tests in these test files. The tests verify that your methods
return the correct PixelPicture objects by comparing them to simple images
constructed from small two-dimensional arrays. The diff
method in the
PixelPicture class is useful for checking that two PixelPicture
objects
have the same bitmaps.
The tests in ImageTest.java
are based on the picture files included in the
images folder in Codio. Note that these tests compare your solution to ours
exactly. Because of floating point imprecision, your code may fail these
tests but still be visually correct. These tests will allow you to see how
close your solution is to ours.
Finally, beware of image compression effects when comparing two images. Due to
image compression, if you save as a jpg
or gif
, the pixels in the saved
image on your hard drive will be slightly altered compared to the
PixelPicture
object (in memory) that is returned from your manipulation
methods. (In the case of gif images, this is due to palette reduction of the
same kind as the one you will implement yourself!) Therefore, you always want to
compare only uncompressed saved images to our sample images.
Task 2: Rotation
Here, your task is to change the orientation of an image.
In SimpleManipulations.java
, there are two rotation functions: rotateCW()
and rotateCCW()
, which rotate an image clockwise and counter-clockwise,
respectively. Each function rotates the image 90° in the given direction.
We have implemented rotateCW
for you; you will implement rotateCCW
.
Implementing this command will require you to process bitmaps. To understand
the two rotations, consider the following bitmap, where we’ve numbered each
pixel with its coordinates:
|
|||||
Original array, pixels numbered with coordinates |
|
|
|||||||||
Clockwise rotation | Counter-clockwise rotation |
Your job is to implement this “renumbering”, copying pixels from their old coordinates to their new coordinates.
For this implementation, you should fill in the definition of the static
method rotateCCW
in SimpleManipulations.java
. Do not merely call
rotateCW
three times.
Task 3: Border
In this task, you will create a new image that adds a border to an existing image.
The first step is to implement the static method border
in
SimpleManipulations.java
. As with rotation, this operation is performed by
copying pixels from their old locations to new coordinates. However, this
time the new image will be larger than the supplied picture because of the
added border.
Task 4: Simple Pixel Transformations
In this task, you will perform some image manipulations that require manipulation of Pixel RGB values.
For this task, you will need to implement several basic pixel transformations
from SimpleManipulations.java
. These manipulations are simple in that they
only require you to consider each pixel independently; you don’t have to
pre-process the image or consider neighboring pixels. As an example of this kind
of transformation, we have given you an implementation of grayScaleLuminosity
.
You will implement the following transformations:
- Color Inversion:
invertColors()
- Grayscaling Via Averaging:
grayScaleAverage()
- Color Scaling:
scaleColors()
with (1.0, 0.5, 0.5)
Color inversion takes each pixel and chooses the “opposite” color of the current one — that is, the one directly across the color wheel.
Grayscaling algorithms transform images from colorful ones to shades of gray; there are several methods of doing this, each of which works best in different situations. We have given you one algorithm and you will be implementing another. An explanation of the specific algorithms can be found in the relevant files.
Color scaling multiplies the color components of each pixel by given scaling
values. For example, with the parameters (1.0, 0.5, 0.5)
, the red components
will be unchanged, but the blue and green parameters will be converted to half
their value. This has the effect of giving the picture a strong red tint and
decreasing the overall brightness.
The transformations you will need to implement all require decomposing each pixel into its three color components: red, green, and blue. Take a look at the included Pixel class for help with this.
d
, you can convert it
to an int by rounding and then casting, using the code (int) Math.round(d)
.
Task 5: Alpha-Blend
In this task you will blend the pixels of another image into the current image.
The next picture manipulation, alphaBlend()
in SimpleManipulations.java
,
actually takes two pictures and combines them pixel-by-pixel to produce a
new image. Both pictures must be of the same dimensions (if they are not,
just return the picture provided as the first argument). The algorithm goes
through the two pictures computing the weighted average of each of the
corresponding pixels in the two images.

This shows the default image blended (alpha = 0.3)
with a grayscale
(average) version of itself. This blend reduces the color saturation of the
image by incorporating some gray into each pixel.
Task 6: Advanced Pixel Transformations
For the next operations, you’ll work in AdvancedManipulations.java
. Each
of these transformations requires you to compute additional information
about the image before it can be executed.
Contrast
First, you’ll change the contrast of a picture by implementing the method
adjustContrast()
in AdvancedManipulations.java
.
Your job is to change the intensity of the colors in the picture, following this simple method of changing contrast:
-
Find the average color intensity of the picture.
- Sum the values of all the color components in all of the pixels (for each pixel add each of its color components to the total sum).
- Divide the total sum by the number of pixels times 3 (the number of components). This is the average color intensity.
-
Subtract the average color intensity from each color component of each pixel, resulting in a “normalized” color component. (This will make the average color intensity for the entire image zero.)
-
Scale each normalized color component by multiplying them by the contrast “multiplier” parameter. Note that the multiplier is a double (a decimal value like 1.2 or 0.6) and normalized color values are integers.
These scaled and normalized color components may be negative or larger than 255, but that is OK! (See below.)
-
Add the original average color intensity back to the scaled, normalized components to create a new pixel. Note that the
Pixel
class will handle clipping of the resulting components to the range 0-255, as desired.

The default image with contrast multiplier of 2.0.
Math.round()
and type casting to properly round it to an int.
Reduced color palette
Next, you’ll reduce a picture to its most common colors by implementing the
method reducePalette()
.
You will need to make use of the ColorMap
class to generate a map from Pixels
of a certain color to the frequency with which identically-colored pixels appear
in the image. Once you have generated your ColorMap
, select your palette by
retrieving the pixels whose color appears in the picture with the highest
frequency. Then, change each pixel in the picture to one with the closest
matching color from your palette. Use the distance method in the Pixel
class
to figure out the difference between two pixels.
Algorithms like this are widely used in image compression. GIFs in particular compress the palette to no more than 255 colors. The variant we have implemented here is a weak one since it only counts color frequency by exact match. Advanced palette reduction algorithms (known as “indexing” algorithms) calculate color regions and distribute the palette over the regions. For example, if our picture had a lot of shades of blue and a little bit of red, our algorithm would likely choose a palette of all blue colors. An advanced algorithm would recognize that blues look similar and distribute the palette so that it would be possible to display red as well.

This shows the default image with a palette reduced to 512 colors
Task 7: Blur
Finally, you will write code to make an image appear blurry.
The blur()
method in AdvancedManipulations.java
takes one argument, a
radius. There are different blurring algorithms; we’ll implement the simplest,
called a box blur. Box blurring works by averaging a box-shaped neighborhood
around a pixel. Be sure to include this pixel in each average. The size of the
box is configurable by setting the radius, half the length of a side of the
box.
In the simplest case — a radius of 1 — blurring just takes the average around a pixel. Here, to blur around the pixel at (1,1) with radius 1, we take the average value of the pixels of its neighborhood: (0,0) through (2,2), including (1,1).
| |||||
Box blur neighborhood around (1,1), radius 1 |
This algorithm must be careful of corner cases. When blurring (0,0) with radius 1, we only need to consider the top-left corner of the images, pixels (0,0) through (1,1), and so we need to divide by 4 at the end, not 9. You’ll have to be careful to only access bitmaps inside of their bounds. You can assume that you will not be given a radius less than 1.

This shows the default image blurred with radius 2.
A Custom Effect (Kudos Problem I)
At this point, you have implemented all of the basic transformations. The
effects on the right side of the GUI should all work, except for the last one.
For this effect, you have the opportunity to design your own filter. Take a look
at how the effects in Effects.java
are implemented and do something cool in the
method custom
. This part of the assignment is worth no points, but we want to
see what you come up with. If you create a particularly nice effect and want to
share with all of us, post the output image (and the source code if you wish) in
a private Ed post.
Flood fill (Kudos Problem II)
The last problem is a challenge. It is here for additional practice but again worth no points on the assignment.
The flood
command is short for “flood fill,” which is the familiar “paint
bucket” operation in graphics programs. In a paint program, the user clicks
on a point in the image. Every neighboring, similarly colored point is then
“flooded” with the color the user selected.
Suppose we want to flood color
at (x,y)
. The simplest way to do flood fill
is as follows.
- Let
target
be the color at(x,y)
. - Create a set of points
Q
containing just the point(x,y)
. - Take the first point
p
out ofQ
. - Set the color at
p
tocolor
. - For each of
p
’s non-diagonal neighbors—up, down, left, and right — check to see if they have the colortarget
. If they do, add them toQ
. - If
Q
is empty, stop. Otherwise, go to step 3.
(Some questions you should ask yourself (but not the TAs!): What happens when
target
and color
are the same? How can you speed up this naïve
algorithm?)
For Q
, you should use the provided PointQueue
class. It works very much like
the queues we implemented in OCaml.
blur(16)
,
contrast(16)
, and flood
.
Submission
If you are using Codio
Submit hw06-submit.zip
containing only:
Pixel.java
MyPixelTest.java
SimpleManipulations.java
AdvancedManipulations.java
ImageTest.java
This zip file will be automatically created with the correct files if you use the “Zip” command in Codio.
If you are using IntelliJ
Gradescope allows you to easily drag-and-drop files into it, or you can click “Browse” in Gradescope to open up a file browser on your computer to select files. Upload only the files listed above.
Grading
Here’s the grade breakdown:
- Pixel: 13 points
- Simple image manipulations (
rotateCCW
,border
): 10 points total - Simple pixel transformations (color inversion, grayscale average, color scaling): 12 points total
- Alpha-Blend: 8 points total
- Contrast: 12 points total
- ReducePalette: 20 points total
- Blur: 20 points total
- Flood fill: 0 (kudos only)
- Style: 5 auto-graded points total
You have five free submissions, after which there will be a five-point penalty for each extra submission.