Short Assignment 3 is due Wednesday.
A digital image is represented as a 2-dimensional grid of pixels, or
picture elements.
(Exactly how the image is represented is hidden inside the class
BufferedImage
.) Draw a rectangle, label upper
left corner (0,0), talk about pixel at (x, y), where x is position
in row, y is position in column. Note that y gets bigger as it goes
down the image.
So what is a pixel? It is basically a representation of the color
that should be drawn on the screen at that point. This color is
represented in RGB, short for Red-Green-Blue. If you ask a
BufferedImage
for the color at a given pixel you get back an
int
. This value is not really treated as an
int, though. Its 32 bits are broken into four 8-bit chunks. Each chunk represents
a number between 0 and 255 in binary.
The leftmost 8 bits are the alpha value. This represents the amount of transparency, with 0 as totally transparent and 255 as totally opaque. The next 8 bits represent the amount of red (0 no red to 255 maximum red), the next 8 bits represent the amount of green, and the rightmost 8 bits represent the amount of blue. We won't be dealing with the alpha value much - all of the images we will be playing with have alpha = 255. (You can play around with it, though.)
Java has built-in graphics and image-handling classes and methods.
We will see some soon. However, Barb Ericson at Georgia Tech has
created a set of classes to allow you to represent and manipulate images
more easily than using the built-in classes. We will use these classes
to manipulate images. In particular you need to know about Picture
,
SimplePicture
, Pixel
, and FileChooser
.
The JavaDocs for these
four classes are in the file doc.zip. Unpack it and
open index.html
to get a directory to the JavaDocs for these
four classes. You should not need to modify or even look at the code for any of
the classes except Picture
.
Picture
is the class that is designed for you to add to or modify.
Most of the work is done in a class
SimplePicture
, and Picture
extends
SimplePicture
. Therefore any method defined in
SimplePicture
can be called on a Picture
object
and it will work correctly. She did this to allow you to use the SimplePicture
class without having to look at its code.
The methods from SimplePicture
that you are likely to use
are explore
, getPixel
, getPixels
, getHeight
, getWidth
, setTitle
, and
show
.
The Pixel
class allows you get and set the colors
within a single pixel in various ways. The methods that you are likely to use are
getRed
, getGreen
, getBlue
, getColor
, setRed
,setGreen
, setBlue
, setColor
, correctValue
, getX
, and getY
. Some of these
methods use the Java library class Color. To learn about the this class go
to the Get Help tag on
course home page, click on the "Documentation for all supplied Java classes" link, scroll
down through "All Classes" and click on "Color". One things that you might want to use from the Color
class is the color constants for the various colors (Color.BLACK
, Color.BLUE
, Color.CYAN
, etc). The methods getRed
, getGreen
, and getBlue
will also be useful. You might expect methods to set red, green, and blue values, but Color
objects are immutable and cannot be changed. You instead use the
constructor Color(int r, int g, int b)
to create a new Color
out of the new red, green, and blue values rather than changing an existing color.
There is also an equals
method that determines if
two Color
objects represent the same color.
Finally, you may find FileChooser
's PickAFile
, getMediaPath
and pickMediaPath
useful.
Important note: The method FileChooser.pickAFile
calls Java's
JFileChooser
method to pop up a dialog box and let you navigate to the appropriate file. It then returns the complete file name as a string and you may use it to open an new Picture
as you did in SA0. It is very convenient, and for most people it works fine. However,
for some Java installations (including mine) the second time FileChooser.pickAFile
is called it goes into an infinite loop, requiring killing the program. The problem seems to be in the call to the JFileChooser
method in the Java library. There are reports of this problem on the web page stackoverflow, but no easy solutions.
Fortunately you often only need to open one picture during a run of your program.
If you need more pictures open and pickAFile
does not work the second time
you can call
FileChooser.getMediaPath("filename")
, where filename is the name of the picture file in your media-sources
folder. This call returns same complete file name that
FileChooser.pickAFile
would return. This is less convenient, but it works.
Our first order of business is to go into DrJava and call FileChooser.pickMediaPath()
from the Interactions window
to select our media-sources
folder. Once this is done, all of our
searches for media will begin at this folder and FileChooser.getMediaPath
will prepend this path the the file name that you supply. We only need to do this once,
and it will be remembered in a file for future use. Reset the interactions window after
doing this, because this method also calls JFileChooser
.
Let's look at the JavaDoc for the Picture
class and play
with some of the functions. We will first figure out what they are doing, and then try to decide how they must work. Then we will see how the
Java code does this.
We have a choice of 5 different constructors. Overloaded! The constructors that we
will use are the one with a String
parameter (the complete file name of
the .jpg file), the one with 2 int parameters, and the copy constructor that takes
a Picture
as its parameter. It is
a way to make a new copy of a picture. (The copy
method is another
way to do the same thing.)
pickAndShow
gives an easy way to get a picture from an
image file, but I prefer to call explore
rather than show
.
Using explore
I can examine the colors of individual pictures. Demo it. showNamed
does the same, but from the file name.
copy
is fairly obvious, although the reason for it may not be.
What advantage is there to copying rather than p2 = p
? (Changing
the copy won't change the original.)
So what does increaseRed
do? Demo on a copy of a Picture
made from beach.jpg
. Looks like it ups the red
value. Maxes out at 255, though. Do enough times every
pixel is saturated with red. (Call explore
to allow you to find
RGB values for any pixel that you click on.) Why not all entirely red?
Have green and blue components. If all are 255, get white! Also, note that if
red was originally 0 it stays 0. Play with decreaseRed
also.
Looks like we need to go into the pixel repesentation and change the red value.
It would be a pain to go in and modify the int
representing each color.
Would have to pull out the correct 8 bits, treat them as a number, double it, and then
put the new value for 8 bits back into the color representation. Not hard, but messy.
Fortuately the Pixel
class will simplify this for us. Go through the
JavaDoc for this class. Note getRed
, setRed
, other useful functions.
Looking at the increaseRed
we can see that it does precisely what we
thought that it did. If calls this.getPixels()
(the this
is
not needed) to get an array of all of the pixels. It then goes through each pixel in a
foreach loop, calling getRed
, doubling it, and using setRed
to
put the value back in the pixel. Note the two occurrences of pixelObj
in the expression.
What does negate
do? Makes it look like a photographic negative
(for those of you who have seen photographic film). Click on pixels using
explore()
, note that the amount of color in a pixel in the original
and the amount of the same color in the negative seem to add to 255. So
subtract from 255 to get new color.
The code for negate
does what we expect it to. It again gets an array
of all the pixels, pulls out the red, green, and blue values, and calls setColor
to store a new Color
object with the appropriate values.
flip
is fairly straightforward. How would you compute it? Reverse
the order of the pixels in each row. Note the flip
returns a
Picture
, so we can say p.flip().explore()
. Work
left to right in this expression. p
is a Picture
.
Call flip
on it, get another Picture
. Call
explore
on that picture.
flip
cannot easily use the approach of getting all of the pixels and processing
them in a foreach loop. It instead goes through each pixel individually in a loop. It
first creates a new Picture
of the correct size and then does a double loop
to handle copying pixels from one picture to the other. The outer loop does something
unusual. It has two indices, going in opposite directions. These will be used for the
column numbers (x-axis) to reverse the pixel order. (How would you do this with a single
loop index?) The inner loop mirrors this construct, but it has two indices that always
have the same value. Perhaps Prof. Ericson thought that this would be clearer. At
any rate, it sets the color of the target pixel to the color of the corresponding source
pixel, running through the rows in opposite directions.
decreaseRed
does the same thing as increaseRed
, except it
halves the red value. It also uses a while
loop instead of a foreach
loop and spreads the computation over several lines. (I would have just done integer
division by 2 instead of multiplying by 0.5 and casting.)
Look at more complex things. compose
replaces part of a picture by
another picture. If bigBen
is the Picture
created from the file bigben.jpg
and beach
is a Picture
made from
beach.jpg
, try bigBen.compose(beach, 200, 20)
. Looks like it copies pixels from the first, saves them in the second, with upper left corner specified.
The code here does what is expected. It uses a double index in the for
loop again, starting the srcX
and srcY
at 0 and the
trgX
and trgY
A similar idea appears in blueScreen
. This is the method used by TV meterologists
to make it appear that they are standing in front of a big weather display
when in fact they are standing in front of a blue background. Demo using
blue-mark.jpg
. If beach
is a Picture
from
beach.jpg
, and blueMark
is a picture from blue-mark.jpg
, then try the call blueMark.blueScreen(beach, 100, 0)
. How does it work? Like compose
,
but only copy non-blue pixels (those with less blue than the sum of red and green).
reduceTo8
shows one way to "posterize" a picture by using a reduced number
of colors. In Lab 1 you will implement a much better way. We will look at this as
an example of how to use ArrayList
s.
Look at the others yourself. The one that changes a picture to black and white (called gray scale) finds a weighted average of the three colors and creates a pixel with all three having that average. (Weighted average because gray scale only shows brightness, and the eye perceives the brightness of the colors differently.) Oil paint method replaces each pixel by the most common color in a rectangle around it, which makes for blotches of color.
If you've programmed in C but not Python, then you're used to arrays. You can access the element at a given index quickly—in constant time, in fact. But if you've programmed in Python but not C, you're used to Python lists, which also allow you to dynamically append, insert, and delete elements at any location in the list. Arrays in Java do not allow you to do these operations unless you code them up yourself. In fact, just appending or inserting into a Java array can be a pain, because arrays in Java have a fixed length. If you want to insert into an array that is full, you need to allocate a new array, copy everything into it, and then insert. And if some other code has a reference to the array, then you need to update that reference. Yuck!
Instead, one of the available classes in Java is the ArrayList
, which is a lot like a Python list. To use it, you have to add the line
import java.util.ArrayList;
to your .java file.
The reduceTo8
method has the declaration with initialization
ArrayList<Pixel> lowValues = new ArrayList<Pixel>(); // a polymorphic ArrayList
When you declare an ArrayList
, you have to say what kinds of objects will be in the ArrayList
. You use Java's generics feature to do so, by putting a class name inside the angle brackets. Here, lowValues
is a reference to an ArrayList
object in which each item is a reference to a
Pixel
object.
Because an ArrayList
is an object, we use Java's new
operator to create it. We still have to use the generics feature when we create the ArrayList
, saying new ArrayList<Pixel>()
. The default constructor for ArrayList
creates an empty ArrayList
object.
reduceTo8
first gets all of the pixels in the picture and puts them in
pixelArray
. It then goes through these pixels and splits them into two groups, those with color value greater than a threshold (126, in this case) and those with
color value less than or equal to that threshold. The call to add
grows the
corresponding ArrayList
by adding the new value to the end. This has to be
done for all three colors, so there is a loop where colorNum
goes from 1 to 3. I wrote versions of getColor
and setColor
that take a
colorNum
and make the call corresponding to that number.
After finding the average high and low values in the ArrayList
s via method calls, the code runs through the pixels (using a foreach loop for the
ArrayList
) and sets each pixel to have the appropriate average color value.
The method averageColors
shows a normal for
loop running
though an ArrayList
. Note that we have to use the method get
instead of square subscript brackets to get a particular pixel out of the ArrayList
. Also note that the number of elements in an ArrayList
is found by calling the method size()
. In contrast, an array uses the
public instance variable length
to get the number of places in the array.
The Java documentation tells you everything about the methods of the ArrayList
class. The following methods are the ones you most frequently use. Here, E
stands for the type that the ArrayList
references (Pixel
in our example).
add(E element)
: appends element
at the end of the ArrayList
.
add(int index, E element)
: inserts element
at position index
, moving all elements with higher indices one position higher.
contains(Object obj)
: returns a boolean indicating whether the ArrayList
contains obj
. (We'll see soon what the Object
class is about.)
indexOf(Object obj)
: return the index of the first occurrence of obj
, or - 1 if obj
is not present in the ArrayList
.
get(int index)
: returns the element at the specified index
. Throws an IndexOutOfBounds
exception if index
is out of bounds.
remove(int index)
: removes the element at position index
, moving all elements with higher indices one position lower and returning the removed element. Throws an IndexOutOfBounds
exception if index
is out of bounds.
remove(Object obj)
: removes the first occurrence of obj
, if it is present in the ArrayList
, returning a boolean indicating whether obj
was present in the ArrayList
.
set(int index, E element)
: replaces the element at position index
by element
, returning the element previously at this position. Throws an IndexOutOfBounds
exception if index
is out of bounds.
size()
: returns the number of elements in the ArrayList
(the equivalent of the length
instance variable of an array).
A word about what the contains
, indexOf
, and remove
methods do. Specifically, how does the ArrayList
decide whether the object obj
is at a particular position p
in ArrayList list
? You might think that the test would be:
obj == list.get(p)
However, that test would ask if they were the exact same object in memory. (That is,
their addresses were the same.) But you would want to say that two of Java's Color
objects are equal if they have the same red, green, blue, and alpha values. (Java does.) So we need a test different from ==
.
Every class has a method named equals
, which returns a boolean indicating whether two objects are "equal." The writer of the class gets to determine what "equal" means, with the default being "are the same object," i.e., the two references in question hold the same address. But you might decide that two distinct objects can be "equal" if, say, some of their instance variables are equal. For example, you could say that two Circle
objects are equal if they have the same center and radius. Anyway, the contains
, indexOf
, and remove
methods call the equals
method of the class that obj
references to decide whether there's a match.
Take a moment to look at the Java documentation to see what other methods the ArrayList
class provides.
Notice how the ArrayList
in the example held references to objects. Could it have held primitive types, such as an int
? After all, an array can hold either primitive types or references. But an ArrayList
cannot; it can hold only references.
But what if you really wanted an ArrayList
that holds int
s? Java provides wrapper classes that allow you to treat primitive types like objects. (If you recall the discussion of the Smalltalk language, it would need no such facility, since everything, and I mean everything, is an object.) The class names for the wrapper classes are Boolean
, Character
, Byte
, Short
, Integer
, Long
, Float
, and Double
. So, if you wanted an ArrayList
holding integer values, you could declare
ArrayList<Integer> intList = new ArrayList<Integer>();
What's nice is that you can now treat this ArrayList
as though each element was in fact an int
, even though each element is really a reference to an Integer
object. For example:
intList.add(7);
int n = intList.get(0);
Java has features called autoboxing and unboxing. When you use an int
where an Integer
object is expected, as in the call to add
, Java automatically converts the int
to an Integer
for you. The Integer
object holds that int
value. That's autoboxing. When you use an Integer
object where an int
is expected, as in the assignment to n
, Java automatically converts the Integer
to an int
; that's unboxing.
There are some subtleties that can mess you up. The following two calls do very different things:
intList.remove(3);
intList.remove(new Integer(3));
The first call removes the element at index 3. The second call removes the first element in the ArrayList
whose value is 3.
A second case that could be confusing is the test: 3 == new Integer(3)
.
Does Java first box the 3 and then compare the two Integer
objects (which might then have different addresses) or does it unbox the Integer
and then compare the two primitives? You can test to see which is the case, but your code should never depend on whether you box or unbox to make things the same type. The safe thing is to only use autoboxing and unboxing to put primitives into and take primitives out of data structures that expect objects.