4.3. Changing Color ValuesThe easiest thing to do with pictures is to change the color values of their pixels by changing the red, green, and blue components. You can get radically different effects by simply tweaking those values. Many of Adobe Photoshop's filters do just what we're going to be doing in this section. The way that we're going to be manipulating colors is by computing a percentage of the original color. If we want 50% of the amount of red in the picture, we're going to set the red channel to 0.50 times whatever it is right now. If we want to increase the red by 25%, we're going to set the red to 1.25 times whatever it is right now. Recall that the asterisk (*) is the operator for multiplication in Java. 4.3.1. Using a For-Each LoopWe know that we can use the getPixels() method to get an array of Pixel objects from a Picture object. We can use the geTRed() method to get the red value from a Pixel object, then we can multiply it by 0.5 to decrease the red value, and then we can use setRed() to set the red value of a Pixel object. We will need to cast back to integer after we multiply the red value by 0.5. Remember that if the computer sees you using a double value it assumes that the result should be a double. However, pixel color values must be integers. We could write the code to change the first three pixels like this: > String fName = "C:/intro-prog-java/mediasources/caterpillar.jpg"; > Picture pict = new Picture(fName); > pict.show(); > Pixel[] pixelArray = pict.getPixels(); > Pixel pixelObj = pixelArray[0]; > int red = pixelObj.getRed(); > red = (int) (red * 0.5); > pixelObj.setRed(red); > pixelObj = pixelArray[1]; > red = pixelObj.getRed(); > red = (int) (red * 0.5); > pixelObj.setRed(red); > pixelObj = pixelArray[2]; > red = pixelObj.getRed(); > red = (int) (red * 0.5); > pixelObj.setRed(red); > pict.explore(); This only changes the first three pixels. We don't want to write out statements like this to change all of the pixels in the array even for a small picture. We need some way to repeat the statements that get the red value, change it, and then set the red value for each pixel in the array. As of Java 5.0 (1.5) we can do that using a for-each loop. A loop is a way to repeat a statement or a block of statements. The syntax for a for-each loop is for (Type variableName : array) You can read this as "first declare a variable that will be used in the body of the loop," then "for each element in the array execute the body of the loop." The body of the loop can be either one statement or a series of statements inside of an open curly brace '{' and a close curly brace '}'. The statements in the body of the loop are indented to show that they are part of the loop. A method that will loop through all the pixels in the current picture and set the red value in each to half the original value is: public void decreaseRed() { Pixel[] pixelArray = this.getPixels(); int value = 0; If you are using Java 5.0 (1.5) or above add the decreaseRed() method to the Picture.java file before the last closing curly brace '}'. Then click the COMPILE ALL button in DrJava to compile the file. You can try this method out by typing the following in the interactions pane. > String fName = "C:/intro-prog-java/mediasources/caterpillar.jpg"; > Picture pict = new Picture(fName); > pict.explore(); > pict.decreaseRed(); > pict.explore(); You can compare the original picture with the changed picture. Use the picture explorer to check that the amount of red was decreased. When you execute pict.decreaseRed() the Java runtime checks the Picture class to see if it has a decreaseRed() method. The Picture class does have this method so it will execute that method and implicitly pass in the Picture object the method was invoked on. The keyword this is used to refer to the object the method was invoked on (the one referred to by the variable pict). The first time through the loop the pixelObj will refer to the first element of the array (the one at index 0). The second time through the loop the pixelObj will refer to the second element of the array (the one at index 1). The last time through the loop the pixelObj will refer to the last element of the array (the one at index (length 1)). For-each loops are very useful for looping through each of the elements in an array. If you are still using Java 1.4, you can't use a for-each loop. You can use a while loop instead. Even if you are using Java 5.0 while loops can help you solve problems that for-each loops can't solve. 4.3.2. Using While LoopsA while loop executes a statement (command) or group of statements in a block (inside open and close curly braces). A while loop continues executing until a continuation test is false. When the continuation test is false execution continues with the statement following the while loop. The syntax for a while loop is: while (test) { /** commands to be done go here */ } Let's talk through the pieces here.
Tell someone to clap their hands 12 times. Did they do it right? How do you know? In order to tell if they did it right, you would have to count each time they clapped, and when they stopped clapping your count would be 12 if they did it right. A loop often needs a counter to count the number of times you want something done and an expression that stops when that count is reached. You wouldn't want to declare the count variable inside the while loop because you want it to change each time through the loop. Typically you declare the count variable just before the while loop and then increment it just before the end of the block of commands you want to repeat.
Figure 4.16. Flowchart of a while loop. |
|
What if we want to change the color of all the pixels in a picture? Picture objects understand the method getPixels(), which returns a one-dimensional array of pixel objects. Even though the pixels are really in a two-dimensional array (a matrix), getPixels() puts the pixels in a one-dimensional array to make them easy to process if we just want to process all the pixels. We can get a pixel at a position in the array using pixelArray[index] with the index starting at 0 and changing each time through the loop by one until it is equal to the length of the array of pixels. Instead of calling the variable "count," we will call it "index" since that is what we are using it for. It doesn't matter to the computer, but it makes the code easier for people to understand.
Here is the while loop that simply sets each pixel's color to black in a picture.
> import java.awt.Color; > String fName = "C:/intro-prog-java/mediasources/caterpillar.jpg"; > Picture pict = new Picture(fName); > pict.show(); > Pixel[] pixelArray = pict.getPixels(); > Pixel pixel = null; > int index = 0; > while (index < pixelArray.length) { pixel = pixelArray[index]; pixel.setColor(Color.black); index++; } > pict.repaint();
Let's talk through this code.
We will be using the Color class so we need to either use the fully qualified name (java.awt.Color) or import the Color class using:
import java.awt.Color;
Next we declare a variable with the name fileName to refer to the string object that has a particular file name stored in it:
C:/intro-prog-java/mediasources/caterpillar.jpg
The variable pict is created and refers to the new Picture object created from the picture information in the file named by the variable fName.
We tell the Picture object to show (display) itself using pict.show();
Next we declare a variable pixelArray that references an array of Pixel objects (Pixel[]). We get the array of Pixel objects by asking the Picture object for them using the getPixels() method.
We declare an object variable, Pixel pixel, that will refer to a pixel object but initialize it to null to show that it isn't referring to any pixel object yet.
We declare a primitive variable index and initialize its value to 0.
Next we have the while loop. First we test if the value of index is less than the length of the array of pixels with while (index < pixelArray.length). While it is, we set the variable pixel to refer to the pixel object at the current value of index in the array of pixel objects. Next we set the color of that pixel to the color black. Finally, we increment the variable index. Eventually the value of the variable index will equal the length of the array of pixels and then execution will continue after the body of the loop. Remember that in an array of five items the valid indexes are 04, so when the index is equal to the length of the array you need to stop the loop.
The statement after the body of the while loop will ask the Picture object pict to repaint so that we can see the color change.
|
Now that we see how to get the computer to do thousands of commands without writing thousands of individual lines, let's do something useful with this.
A common desire when working with digital pictures is to shift the redness (or greenness or bluenessbut most often, redness) of a picture. You might shift it higher to "warm" the picture, or to reduce it to "cool" the picture or deal with overly-red digital cameras.
The method below decreases the amount of red by 50% in the current picture.
/** * Method to decrease the red by half in the * current picture */ public void decreaseRed() { Pixel[] pixelArray = this.getPixels(); Pixel pixel = null; int value = 0; int index = 0; // loop through all the pixels while(index < pixelArray.length) { // get the current pixel pixel = pixelArray[index]; |
Go ahead and type the above into your DrJava definitions pane before the last curly brace in the Picture.java file. Click COMPILE ALL to get DrJava to compile the new method. Why do we have to compile the file before we can use the new method? Computers don't understand the Java source code directly. We must compile it, which translates the class definition from something people can read and understand into something a computer can read and understand.
|
Unlike some other computer languages, Java doesn't compile into machine code, which is the language for the machine it is running on. When we compile Java source code we compile it into a language for a virtual machine, which is a machine that doesn't necessarily exist.
When we successfully compile a ClassName.java file the compiler outputs a ClassName.class file which contains the instructions that a Java virtual machine can understand. If our compile is not successful we will get error messages that explain what is wrong. We have to fix the errors and compile again before we can try out our new method.
When we execute a Java class the Java Virtual Machine will read the compiled code and map the instructions for the virtual machine to the machine it is currently executing on. This allows you to compile Java programs on one type of computer and run them on another without having to recompile.
|
This program works on a Picture objectthe one that we'll use to get the pixels from. To create a Picture object, we pass in the filename. After we ask the picture to decreaseRed(), we'll want to repaint the picture to see the effect. Therefore, the decreaseRed method can be used like this:
> String fName = "C:/intro-prog-java/mediasources/caterpillar.jpg"; > Picture picture = new Picture(fName); > picture.show(); > picture.decreaseRed(); > picture.repaint();
|
The original picture and its red-decreased version appear in Figure 4.17. 50% is obviously a lot of red to reduce! The picture looks like it was taken through a blue filter.
|
|
Let's trace the method to decrease red and see how it worked. We want to start tracing at the point where we just called decreaseRed()
> String fileN = "C:/intro-prog-java/mediasources/caterpillar.jpg"; > Picture picture = new Picture(fileN); > picture.show(); > picture.decreaseRed();
What happens now? picture.decreaseRed() really means invoking the decreaseRed method which you have just added to the Picture.java file on the Picture object referred to by the variable picture. The picture object is implicitly passed to the decreaseRed method and can be referenced by the keyword this. What does "implicitly passed" mean? It means that even though decreaseRed doesn't have any parameters listed it is passed the Picture object it was invoked on. So, picture.decreaseRed() is like decreaseRed(Picture this). All object methods (methods without the keyword static in them) are implicitly passed the object that they are invoked on and that object can be referred to as this.
The first line we execute in Program 5 (page 97) is Pixel[] pixelArray = this.getPixels(). Let's break this down.
The Pixel[] pixelArray is a declaration of a variable pixelArray that references an array of Pixel objects. The '=' means that the variable pixelArray will be initialized to the result of the right side expression which is a call to the method this.getPixels() which returns a one-dimensional array of Pixel objects in the current Picture object.
The this is a keyword that represents the current object. Since the method declaration doesn't have the keyword static in it this is an object method. Object methods are always implicitly passed the current object (the object the method was invoked on). In this case the method decreaseRed() was invoked by picture.decreaseRed(); so the Picture object referenced by the variable picture is the current object. We could leave off the this and get the same result. If you don't reference any object when invoking a method the compiler will assume you mean the current object (referenced by the this keyword).
The this.getPixels() invokes the method getPixels() on the current object. This method returns a one-dimensional array of Pixel objects which are the pixels in the current Picture object.
So at the end of the first line we have a variable pixelArray that refers to an array of Pixel objects. The Pixel objects came from the Picture object which was referred to as picture in the interaction pane and as this in the method decreaseRed().
Next is a declaration of a couple of variables that we will need in the for loop. We will need something to represent the current Pixel object so we declare a variable pixel of type Pixel by Pixel pixel =. We start it off referring to nothing by using the defined value null. We also will need a variable to hold the current red value and we declare that as int value = 0;. We initialize the variable value to be 0. Finally we declare a variable to be the index into the array and the value that changes in the loop int index = 0;. Remember that array elements are indexed starting with 0 and ending at the length of the array minus one.
Variables that you declare inside methods are not automatically initialized for you, so you should initialize them when you declare them.
|
Next comes the loop while (index < pixelArray.length). This tests whether the value of the variable index is less than the length of the array of pixels referred to by pixelArray. If the test is true, the body of the loop will be executed. The body of the loop is all the code between the open and close curly braces following the test. If the test is false, execution continues after the body of the loop.
In the body of the loop we have pixel = pixelArray[index];. This will set the pixel variable to point to a Pixel object in the array of pixels with an index equal to the current value of index. Since index is initialized to 0 before the loop, the first time through this loop the pixel variable will point to the first Pixel object in the array.
Next in the body of the loop is value = pixel.getRed();. This sets the variable value to the amount of red in the current pixel. Remember that the amount of red can vary from a minimum of 0 to a maximum of 255.
Next in the body of the loop is value = (int) (value * 0.5);. This sets the variable value to the integer amount that you get from multiplying the current contents of value by 0.5. The (int) is a cast to integer so that the compiler doesn't complain about losing precision since we are storing a floating point number in an integer number. Any numbers after the decimal point will be discarded. We do this because colors are represented as integers. The (int) (value * 0.5) is needed because the variable value is declared of type int and yet the calculation of (value * 0.5) contains a floating point number and so will automatically be done in floating point. However, a floating point result (say of 1.5) won't fit into a variable of type int. So, the compiler won't let us do this without telling it that we really want it to by including the (int). This is called casting and is required whenever a larger value is being placed into a smaller variable. So if the result of a multiplication has a fractional part, that fractional part will just be thrown away so that the result can fit in an int.
The next step in the body of the loop is pixel.setRed(value);. This changes the amount of red in the current pixel to be the same as what is stored in variable value. The current pixel is the first one, so we see that the red value has changed from 252 to 126 after this line of code is executed.
After the statements in the body of the loop are executed the index = index + 1; will be executed which will add one to the current value of index. Since index was initialized to 0 this will result in index holding the value 1.
What happens next is very important. The loop starts over again. The continuation test will again check that the value in variable index is less than the length of the array of pixels, and since the value of index is less than the length of the array, the statements in the body of the loop will be executed again. The variable pixel will be set to the pixel object in the array of pixels at index 1. This is the second Pixel object in the array pixelArray.
The variable value will be set to the red amount in the current pixel referred to by the variable pixel, which is 253.
The variable value will be set to the result of casting to integer the result of multiplying the amount in value by 0.5. This results in (253 * 0.5) = 126.5 and after we drop the digits after the decimal this is 126. We drop the digits after the decimal point because of the cast to the type int (integer). We cast to integer because colors are represented as integer values from 0 to 255.
The red value in the current pixel is set to the same amount as what is stored in value. So the value of red in the second pixel changes from 253 to 126.
The variable index is set to the result of adding 1 to its current value. This adds 1 to 1, resulting in 2.
At the end of the loop body we go back to the continuation test. The test will be evaluated and if the result is true the commands in the loop body will be executed again. If the continuation test evaluates to false execution will continue with the first statement after the body of the loop.
Eventually, we get Figure 4.17 (and at Figure 4.18). We keep going through all the pixels in the sequence and changing all the red values.
How do we know that that really worked? Sure, something happened to the picture, but did we really decrease the red? By 50%?
|
We can check it several ways. One way is with the picture explorer. Create two Picture objects: Picture p = new Picture(FileChooser.pickAFile()); and Picture p2 = new Picture(FileChooser.pickAFile()); and pick the same picture each time. Decrease red in one of them. Then open a picture explorer on each of the Picture objects using p.explore(); and p2.explore();.
We can also use the methods that we know in the Interactions pane to check the red values of individual pixels.
> String fName = "C:/intro-prog-java/mediasources/caterpillar.jpg"; > Picture pict = new Picture(fName); > Pixel pixel = pict.getPixel(0,0); > System.out.println(pixel); Pixel red=252 green=254 blue=251 > pict.decreaseRed(); > Pixel newPixel = pict.getPixel(0,0); > System.out.println(newPixel); Pixel red=126 green=254 blue=251 > System.out.println( 252 * 0.5); 126.0
Let's increase the red in the picture now. If multiplying the red component by 0.5 decreased it, multiplying it by something over 1.0 should increase it. I'm going to apply the increase to the exact same picture, to see if we can reduce the blue (Figure 4.19).
/** * Method to increase the amount of red by 30% */ public void increaseRed() { Pixel[] pixelArray = this.getPixels(); Pixel pixel = null; |
This method works much the same way as the method decreaseRed. We set up some variables that we will need such as the array of pixel objects, the current pixel, the current value, and the current index. We loop through all the pixels in the array of pixels and change the red value for each pixel to 1.3 times its original value.
|
Compile the new method increaseRed and first use decreaseRed and then increaseRed on the same picture. Explore the picture objects to check that increaseRed worked. Remember that the method explore makes a copy of the picture and allows you to check the color values of individual pixels.
> String fName = "C:/intro-prog-java/mediasources/caterpillar.jpg"; > Picture picture = new Picture(fName); > picture.decreaseRed(); > picture.explore(); > picture.increaseRed(); > picture.explore();
We can even get rid of a color completely. The method below erases the blue component from a picture by setting the blue value to 0 in all pixels (Figure 4.20).
/** * Method to clear the blue from the picture (set * the blue to 0 for all pixels) */ public void clearBlue() { Pixel[] pixelArray = this.getPixels(); Pixel pixel = null; int index = 0; // loop through all the pixels while (index < pixelArray.length) { // get the current pixel pixel = pixelArray[index]; // set the blue on the pixel to 0 pixel.setBlue(0); // increment index index++; } } |
Compile the new method clearBlue and invoke it on a Picture object. Explore the picture object to check that all the blue values are indeed 0.
> String fName = "C:/intro-prog-java/mediasources/caterpillar.jpg"; > Picture picture = new Picture(fName);
[Page 109] > picture.explore(); > picture.clearBlue(); > picture.explore();
This method is also similar to the decreaseRed and increaseRed methods except that we don't need to get out the current blue value since we are simply setting all the blue values to 0.
We can certainly do more than one color manipulation at once. Mark wanted to try to generate a sunset out of a beach scene. His first attempt was to increase the red, but that doesn't always work. Some of the red values in a given picture are pretty high. If you go past 255 for a channel value it will keep the value at 255.
His second thought was that maybe what happens in a sunset is that there is less blue and green, thus emphasizing the red, without actually increasing it. Here was the program that he wrote for that:
/** * Method to simulate a sunset by decreasing the green * and blue */ public void makeSunset() { Pixel[] pixelArray = this.getPixels(); Pixel pixel = null; int value = 0; int i = 0; // loop through all the pixels while (i < pixelArray.length) { // get the current pixel pixel = pixelArray[i]; // change the blue value value = pixel.getBlue(); pixel.setBlue((int) (value * 0.7)); // change the green value value = pixel.getGreen(); pixel.setGreen((int) (value * 0.7)); // increment the index i++; } } |
|
Compile the new method makeSunset and invoke it on a Picture object. Explore the picture object to check that the blue and green values have been decreased.
> String fName = "C:/intro-prog-java/mediasources/beach-smaller.jpg"; > Picture picture = new Picture(fName); > picture.explore(); > picture.makeSunset(); > picture.explore();
What we see happening in Program 8 (page 109) is that we're changing both the blue and green channelsreducing each by 30%. The effect works pretty well, as seen in Figure 4.21.
You probably have lots of questions about methods at this point. Why did we write these methods this way? How is that we're reusing variable names like pixel in every method? Are there other ways to write these methods? Is there such a thing as a better or worse method?
Since we're always picking a file (or typing in a filename) then making a picture, before calling one of our picture manipulation methods, and then showing or repainting the picture, it's a natural question why we're not building those in. Why doesn't every method have String fileName = FileChooser.pickAFile(); and new Picture(fileName); in it?
We actually want to write the methods to make them more general and reusable. We want our methods to do one and only one thing, so that we can use the method again in a new context where we need that one thing done. An example might make that clearer. Consider the program to make a sunset (Program 8 (page 109)). That works by reducing the green and blue, each by 30%. What if we rewrote that method so that it called two smaller methods that just did the two pieces of the manipulation? We'd end up with something like Program 9 (page 111).
/** * Method to decrease the green in the picture by 30% */ public void decreaseGreen() { Pixel[] pixelArray = this.getPixels(); Pixel pixel = null; int value = 0; int i = 0; // loop through all the pixels in the array while (i < pixelArray.length) { // get the current pixel pixel = pixelArray[i]; // get the value value = pixel.getGreen(); // set the green value to 70% of what it was pixel.setGreen((int) (value * 0.7)); // increment the index i++; } } /** * Method to decrease the blue in the picture by 30% */ public void decreaseBlue() { Pixel[] pixelArray = this.getPixels(); Pixel pixel = null; int value = 0; int i = 0; // loop through all the pixels in the array while (i < pixelArray.length) { |
The first thing to note is that this actually does work. makeSunset2() does the same thing here as in the previous method. It's perfectly okay to have one method (makeSunset2() in this case) use other methods in the same class (decreaseBlue() and decreaseGreen()). You use makeSunset2() just as you had before. It's the same algorithm (it tells the computer to do the same thing), but with different methods. The earlier program did everything in one method, and this one does it in three. In fact, you can also use decreaseBlue() and decreaseGreen() by themselves toomake a picture in the Command Area and invoke either method on the Picture object. They work just like decreaseRed().
What's different is that the method makeSunset2() is much simpler to read. That's very important.
|
What if we had written decreaseBlue() and decreaseGreen() so that each asked you to pick a file and created the picture before changing the color. We would be asked to pick a file twiceonce in each method. Because we wrote these methods to only decrease the blue and decrease the green ("one and only one thing") in the implicitly passed Picture object, we can use them in new methods like makeSunset().
There is an issue that the new makeSunset2() will take twice as long to finish as the original makeSunset(), since every pixel gets changed twice. We address that issue in Chapter 15 on speed and complexity. The important issue is still to write the code readably first, and worry about efficiency later. However, this could also be handled by a method that changes each color by some passed in amount. This would be a very general and reusable method.
Now, let's say that we asked you to pick a picture and created the picture in makeSunset2() before calling the other methods. The methods decreaseBlue() and decreaseGreen() are completely flexible and reusable again. But the method makeSunset2() is now less flexible and reusable. Is that a big deal? No, not if you only care about having the ability to give a sunset look to a single picked picture. But what if you later want to build a movie with a few hundred frames of Picture objects, to each of which you want to add a sunset look? Do you really want to pick out each of those few hundred frames? Or would you rather write a method to go through each of the frames (which we'll learn how to do in a few chapters) and invoke makeSunset2() on each Picture object. That's why we make methods general and reusableyou never know when you're going to want to use that method again, in a larger context.
|
Even larger methods, like makeSunset(), do "one and only one thing." The method makeSunset() makes a sunset-looking picture. It does that by decreasing green and decreasing blue. It calls two other methods to do that. What we end up with is a hierarchy of goalsthe "one and only one thing" that is being done. makeSunset() does its one thing, by asking two other methods to do their one thing. We call this hierarchical decomposition (breaking down a problem into smaller parts, and then breaking down those smaller parts until you get something that you can easily program), and it's very powerful for creating complex programs out of pieces that you understand. This is also called top-down refinement or problem decomposition.
Names in methods are completely separate from names in the interactions pane and also from names in other methods. We say that they have different scope. Scope is the area where a name is known by the computer. Variables declared inside of a method have method scope and only apply inside that method. That is why we can use the same variable names in several methods. Variables declared inside the Interactions Pane are known inside the Interactions Pane until it is reset. This is why you get Error: Redefinition of 'picture' when you declare a variable that is already declared in the Interactions Pane.
The only way to get any data (pictures, sounds, filenames, numbers) from the interactions pane into a method is by passing it in as input to the method. Within the method, you can use any names you wantnames that you first define within the method (like pixel in the last example) or names that you use to stand for the input data (like fileName) only exist while the method is running. When the method is done, those variable names literally do not exist anymore.
This is really an advantage. Earlier, we said that naming is very important to computer scientists: We name everything, from data to methods to classes. But if each name could mean one and only one thing ever, we'd run out of names. In natural language, words mean different things in different contexts (e.g., "What do you mean?" and "You are being mean!"). A method is a different contextnames can mean something different than they do outside of that method.
Sometimes, you will compute something inside a method that you want to return to the interactions pane or to a calling method. We've already seen methods that output a value, like FileChooser.pickAFile() which outputs a filename. If you created a Picture object using new Picture(fileName) inside a method, you should return it so that it can be used. You can do that by using the return keyword.
The name that you give to a method's input can be thought of as a placeholder. Whenever the placeholder appears, imagine the input data appearing instead. So, in a method like:
/** * Method to change the red by an amount * @param amount the amount to change the red by */ public void changeRed(double amount) { Pixel[] pixelArray = this.getPixels(); Pixel pixel = null; int value = 0; int i = 0; // loop through all the pixels while( i < pixelArray.length) { // get the current pixel pixel = pixelArray[i]; // get the value value = pixel.getRed(); /* set the red value to the original value * times the passed amount */ pixel.setRed((int) (value * amount)); |
When you call (invoke) the method changeRed with a specific amount such as picture.changeRed(0.7); it will decrease the red by 30%. In the method changeRed the input parameter amount is set to 0.7. This is similar to declaring a variable inside the method like this double amount = 0.7;. Just like any variable declared in the method the parameter amount is known inside the method. It has method scope.
Call changeRed with an amount less than one to decrease the amount of red in a picture. Call changeRed with an amount greater than one to increase the amount of red in a picture. Remember that the amount of red must be between 0 and 255. If you try to set the amount of red less than 0 it will be set to 0. If you try to set the amount of red greater than 255 it will be set to 255.
We've talked about different ways of writing the same methodsome better, some worse. There are others that are pretty much equivalent, and others that are much better. Let's consider a few more ways that we can write methods.
We can pass in more than one input at a time. Consider the following:
/** * Method to change the color of each pixel in the picture * object by passed in amounts. * @param redAmount the amount to change the red value * @param greenAmount the amount to change the green value * @param blueAmount the amount to change the blue value */ public void changeColors(double redAmount, double greenAmount, double blueAmount) { Pixel[] pixelArray = this.getPixels(); Pixel pixel = null; int value = 0; int i = 0; // loop through all the pixels while( i < pixelArray.length) { // get the current pixel pixel = pixelArray[i]; // change the red value value = pixel.getRed(); pixel.setRed((int) (redAmount * value)); |
We could use this method as shown below:
> String fName = "C:/intro-prog-java/mediasources/beach-smaller.jpg"; > Picture picture = new Picture(fName); > picture.changeColors(1.0,0.7,0.7); > picture.show();
The above code would have the same result as makeSunset(). It keeps the red values the same and decreases the green and blue values 30%. That's a pretty useful and powerful method.
Recall seeing in Program 7 (page 108) this code:
/** * Method to clear the blue from the picture (set * the blue to 0 for all pixels) */ public void clearBlue() { Pixel[] pixelArray = this.getPixels(); Pixel pixel = null; int index = 0; // loop through all the pixels while (index < pixelArray.length) { // get the current pixel pixel = pixelArray[index]; // set the blue on the pixel to 0 pixel.setBlue(0); // increment index index++; } }
We could also write that same algorithm like this:
/** * Method to clear the blue from the picture (set * the blue to 0 for all pixels) */ public void clearBlue2() { Pixel[] pixelArray = this.getPixels(); int i = 0; // loop through all the pixels while(i < pixelArray.length) { pixelArray[i].setBlue(0); i++; } }
It's important to note that this method achieves the exact same thing as the earlier method did. Both set the blue channel of all pixels to zero. An advantage of the second method is that it is shorter and doesn't require a variable declaration for a pixel. However, it may be harder for someone to understand. A shorter method isn't necessarily better.
You may have had the problem that you forgot to declare the index variable before you tried to use it in your while loop. You may also have had the problem of forgetting to increment the index variable before the end of the loop body. This happens often enough that another kind of loop is usually used when you want to loop a set number of times. It is called a for loop.
A for loop executes a command or group of commands in a block. A for loop allows for declaration and/or initialization of variables before the loop body is first executed. A for loop continues executing the loop body while the continuation test is true. After the end of the body of the loop and before the continuation test one or more variables can be changed.
The syntax for a for loop is:
for (initialization area; continuation test; change area) { /* commands in body of the loop */ }
Let's talk through the pieces here.
First comes the required Java keyword for.
Next we have a required opening parenthesis.
Next is the initialization area. You can declare and initialize variables here. For example, you can have int i=0 which declares a variable i of the primitive type int and initializes it to 0. You can initialize more than one variable here by separating the initializations with commas. You are not required to have any initializations here.
Next comes the required semicolon.
Next is the continuation test. This holds an expression that returns true or false. When this expression is true the loop will continue to be executed. When this test is false the loop will finish and the statement following the body of the loop will be executed.
Next comes the required semicolon.
Next is the change area. Here you usually increment or decrement variables, such as i++ to increment i. The statements in the change area actually take place after each execution of the body of the loop.
Next is the required closing parenthesis.
If you just want to execute one statement (command) in the body of the loop, it can just follow on the next line. It is normally indented to show that it is part of the for loop. If you want to execute more than one statement in the body of the for loop, you will need to enclose the statements in a block (a set of open and close curly braces).
|
Compare the flowchart (Figure 4.22) for a for loop with the flowchart for a while loop (Figure 4.16). They look the same because for loops and while loops execute in the same way even though the code looks different. Any code can be written using either. The syntax of the for loop just makes it easier to remember to declare a variable for use in the loop and to change it each time through the loop since all of that is written at the same time that you write the test. To change clearBlue() to use a for loop simply move the declaration and initialization of the index variable i to be done in the initialization area and the increment of i to be done in the change area.
/** * Method to clear the blue from the picture (set * the blue to 0 for all pixels) */ public void clearBlue3() { Pixel[] pixelArray = this.getPixels(); // loop through all the pixels |
To lighten or darken a picture is pretty simple. It's the same pattern as we saw previously, but instead of changing a color component, you change the overall color. Here's lightening and then darkening as methods. Figure 4.23 shows the lighter and darker versions of the original picture seen earlier.
/** * Method to lighten the colors in the picture */ public void lighten() { Pixel[] pixelArray = this.getPixels(); Color color = null; Pixel pixel = null; // loop through all the pixels for (int i = 0; i < pixelArray.length; i++) { // get the current pixel pixel = pixelArray[i]; |
/** * Method to darken the color in the picture */ public void darken() { Pixel[] pixelArray = this.getPixels(); Color color = null; Pixel pixel = null; // loop through all the pixels for (int i = 0; i < pixelArray.length; i++) { // get the current pixel pixel = pixelArray[i]; // get the current color color = pixel.getColor(); // get a darker color color = color.darker(); // set the pixel color to the darker color pixel.setColor(color); } } |
Creating a negative image of a picture is much easier than you might think at first. Let's think it through. What we want is the opposite of each of the current values for red, green, and blue. It's easiest to understand at the extremes. If we have a red component of 0, we want 255 instead. If we have 255, we want the negative to have a zero.
Now let's consider the middle ground. If the red component is slightly red (say, 50), we want something that is almost completely redwhere the "almost" is the same amount of redness in the original picture. We want the maximum red (255), but 50 less than that. We want a red component of 255 50 = 205. In general, the negative should be 255 original. We need to compute the negative of each of the red, green, and blue components, then create a new negative color, and set the pixel to the negative color.
Here's the program that does it, and you can see from the image that it really does work (Figure 4.24).
/** * Method to negate the picture */ public void negate() { Pixel[] pixelArray = this.getPixels(); Pixel pixel = null; int redValue, blueValue, greenValue = 0; // loop through all the pixels for (int i = 0; i < pixelArray.length; i++) { // get the current pixel pixel = pixelArray[i]; |
Converting to grayscale is a fun program. It's short, not hard to understand, and yet has such a nice visual effect. It's a really nice example of what one can do easily yet powerfully by manipulating pixel color values.
Recall that the resultant color is gray whenever the red component, green component, and blue component have the same value. That means that our RGB encoding supports 256 levels of gray from, (0, 0, 0) (black) to (1, 1, 1) through (100, 100, 100) and finally (255, 255, 255). The tricky part is figuring out what the replicated value should be.
What we want is a sense of the intensity of the color. It turns out that it's pretty easy to compute: We average the three component colors. Since there are three components, the formula for intensity is:
This leads us to the following simple program and Figure 4.25.
/** * Method to change the picture to grayscale */ public void grayscale() { Pixel[] pixelArray = this.getPixels(); Pixel pixel = null; int intensity = 0; // loop through all the pixels for (int i = 0; i < pixelArray.length; i++) { // get the current pixel pixel = pixelArray[i]; // compute the intensity of the pixel (average value) intensity = (int) ((pixel.getRed() + pixel.getGreen() + pixel.getBlue()) / 3); // set the pixel color to the new color pixel.setColor(new Color(intensity,intensity,intensity)); } } |
This is an overly simply notion of grayscale. Below is a program that takes into account how the human eye perceives luminance. Remember that we consider blue to be darker than red, even if there's the same amount of light reflected off. So we weight blue lower, and red more, when computing the average.
/** * Method to change the picture to grayscale with luminance */ public void grayscaleWithLuminance() { Pixel[] pixelArray = this.getPixels(); Pixel pixel = null; int luminance = 0; double redValue = 0; double greenValue = 0; double blueValue = 0; // loop through all the pixels for (int i = 0; i < pixelArray.length; i++) { |