inheritance - university of richmonddszajda/classes/...— these differences will be handled when we...

8
CMSC 150 Lab 8, Part II: Little PhotoShop of Horrors, Part Deux 10 Nov 2015 By now you should have completed the Open/Save/Quit portion of the menu options. Today we are going to finish implementing the three image processing algorithms discussed during lecture over the last few days: invert: essentially make a photographic-negative version of an image; box blur: make a blurred version of the image by averaging pixel values in a box neighborhood; and contrast stretch: stretch the distribution of gray (or color) levels to add more contrast to an image (this part is for extra credit). During this implementation, the only new concept will be a gentle introduction to inheritance in Java. 1) Invert: (Though this was discussed in the Lab 8, Part I handout, I have included it here for complete- ness.) First we want to tackle the “Invert” option under the “Image” menu. We want this to be an in-place operation, meaning as soon as “Invert” is selected, the photographic negative of the image will be displayed in the main window as depicted in the example below. As discussed recently, to accomplish the invert operation, you simply walk through the image’s array pixel- by-pixel (use doubly-nested for loops — refer to last week’s lab if you need a refresher) changing the pixel value to 255 minus its current value. Do this for each of the three color bands red, green, and blue. (This will cause the invert to work for both color and grayscale images. For a grayscale image, at a given pixel the amounts of red, green, and blue are the same, corresponding to a gray level between black and white based on the [identical] intensities of red, green, and blue at that pixel.) Set the SImage instance variable to reference the new inverted image. (This way, if you choose “Invert” again, the image displayed will be just like the original.) 1

Upload: others

Post on 22-Jun-2020

2 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: inheritance - University of Richmonddszajda/classes/...— these differences will be handled when we create new (extending) classes for each preview. Now in the BlueJ main window,

CMSC 150 Lab 8, Part II: Little PhotoShop of Horrors, Part Deux 10 Nov 2015

By now you should have completed the Open/Save/Quit portion of the menu options. Today we are going tofinish implementing the three image processing algorithms discussed during lecture over the last few days:

• invert: essentially make a photographic-negative version of an image;

• box blur: make a blurred version of the image by averaging pixel values in a box neighborhood; and

• contrast stretch: stretch the distribution of gray (or color) levels to add more contrast to an image (thispart is for extra credit).

During this implementation, the only new concept will be a gentle introduction to inheritance in Java.

1) Invert: (Though this was discussed in the Lab 8, Part I handout, I have included it here for complete-ness.) First we want to tackle the “Invert” option under the “Image” menu. We want this to be an in-placeoperation, meaning as soon as “Invert” is selected, the photographic negative of the image will be displayedin the main window as depicted in the example below.

As discussed recently, to accomplish the invert operation, you simply walk through the image’s array pixel-by-pixel (use doubly-nested for loops — refer to last week’s lab if you need a refresher) changing the pixelvalue to 255 minus its current value. Do this for each of the three color bands red, green, and blue. (Thiswill cause the invert to work for both color and grayscale images. For a grayscale image, at a given pixel theamounts of red, green, and blue are the same, corresponding to a gray level between black and white basedon the [identical] intensities of red, green, and blue at that pixel.)

Set the SImage instance variable to reference the new inverted image. (This way, if you choose “Invert”again, the image displayed will be just like the original.)

1

Page 2: inheritance - University of Richmonddszajda/classes/...— these differences will be handled when we create new (extending) classes for each preview. Now in the BlueJ main window,

Test your “Invert” on both grayscale and color images (samples provided on the course Web page).

2) Box Blur: Now we want to tackle the “Box Blur” option. This will not be an in-place operation; rather,we want to open a separate preview window in which we can experiment with the blurring before acceptingthe result to be displayed in the main window.

Before diving in, let’s think ahead. Not only will we want a separate preview window for the box blur, wewill also want one for the contrast stretch. Now is a good time to think about what functionality the twoseparate previews will share, and make good design decisions to facilitate the sharing.

Shown below on the left is an example of what the box blur preview window might look like; an example ofthe contrast stretch preview window is shown below right.

The similarities between the two windows are:

• both will contain two JButton objects for accepting or canceling the blurred result;

• both will contain a JLabel to display an SImage;

• both will contain a JSlider for experimenting with an input parameter to the algorithm, and anassociated JTextField for displaying and/or inputting the JSlider value;

The differences between the two windows are:

• different labels (“Blur Radius:” or “Contrast: ”) at the lower left;

• different min, max, and initial values for the sliders;

• (obviously) different algorithms to execute when the slider values change.

It would be nice if we had a general “preview window” class that implemented only the similarities of the twopreviews. We could then write two separate classes (one for the box blur preview and one for the contraststretch preview) — each would inherit all the properties and functionality from the general class, so that theonly thing to implement in each class would be the functionality unique to that specific preview. This basicidea is known as inheritance.

One mechanism that Java provides for allowing inheritance is to define a class as “abstract”, similar to thefollowing:

public abstract class PreviewWindow extends GUIManager

2

Page 3: inheritance - University of Richmonddszajda/classes/...— these differences will be handled when we create new (extending) classes for each preview. Now in the BlueJ main window,

Notice the keyword abstract in the class declaration. An abstract class is a class for which you cannotdirectly create instances. Instead, you must write a separate (non-abstract) class that extends the abstractclass, and then you can create instances of the new class. Furthermore, in the abstract class, Java allows youto declare abstract methods similar to the following:

public abstract void sliderChanged();

Notice the keyword abstract in the method declaration. Also notice that the declaration ends immediatelywith a semicolon — there is no body defined in the abstract class for this method. Java will then force anyclass that extends this abstract class to provide a definition (i.e., a body) for the abstract method.

On the course Web page, we have provided an abstract class named PreviewWindow. Download the filePreviewWindow.java and add it as a new class from the downloaded file. Open the class in the editorwindow, and you will see the abstract class declaration as well as two abstract method declarations (nobodies). This abstract class implements the commonalities of the two different preview windows, namely it

• creates a new window with a BorderLayout;

• stores as an instance variable the SImage provided as input to the constructor;

• creates a new JLabel with the SImage as its icon and places the label in the center of the content pane;

• creates two JButton objects for accepting or canceling the blurring process and places the buttons inthe top of the content pane and implements the appropriate buttonClicked() method. (Note that thedetails for what occurs when the accept button is clicked are omitted. You will add these details later.)

This class does not construct the JSlider portion because the values will be different in each preview window— these differences will be handled when we create new (extending) classes for each preview.

Now in the BlueJ main window, right-click on the PreviewWindow class and try to create a new instance ofthe class. BlueJ will not let you — you get no “new PreviewWindow()” option even though the class containsa constructor. This is because the class is abstract, and (as stated above) instances of an abstract class cannotbe created.

Let’s start by creating a new class named BoxBlurPreviewWindow that extends PreviewWindow. Modify thedefault class code provided by BlueJ until the definition for you class looks like the following:

import javax.swing.*;

import squint.*;

public class BoxBlurPreviewWindow extends PreviewWindow

{

public BoxBlurPreviewWindow( SImage inputImage )

{

}

}

Now try to compile the BoxBlurPreviewWindow class. You will receive an error (similar to one you sawat the bottom of page 2 yesterday) saying something about “does not override abstract method. . .”. Recallabove we said that Java will force any class extending an abstract class to provide definitions for all abstractmethods. Look again at the bottom of the PreviewWindow class — the two methods sliderChanged() andtextEntered() are declared as abstract, and so we must implement these two methods in our new class.

For now, don’t worry about the details of these two methods (we will address them below), but just putskeleton definitions in your new class, leaving out the abstract keyword:

3

Page 4: inheritance - University of Richmonddszajda/classes/...— these differences will be handled when we create new (extending) classes for each preview. Now in the BlueJ main window,

public void sliderChanged()

{

}

public void textEntered()

{

}

Now compile the class again. You no longer get an error about “does not override” — defining our newnon-abstract methods fixed that problem. However, now you get an error that Java cannot find “constructorPreviewWindow()”. From this, it should be clear that Java knows our new class extends PreviewWindow

and so in our own constructor is trying to automatically call the default constructor for PreviewWindow (i.e.,the constructor that accepts no arguments). Look at PreviewWindow in the editor — there is no defaultconstructor, but rather a constructor that expects an SImage as an argument.

Rather than have Java try to automatically call a constructor for PreviewWindow, we need to explicitly tellJava to invoke the constructor expecting an SImage. We do this by including the following as the first line ofour constructor:

super( inputImage );

In the constructor of a class that extends another (parent) class, the method call super() invokes the con-structor in that parent class. Because the constructor in our parent class PreviewWindow expects an SImage

(which it stores as an instance variable), we pass in the SImage parameter passed to our own constructor.

Now compile the BoxBlurPreviewWindow class again — it should compile successfully. In the main BlueJwindow, right-click on the class. Notice that, unlike for the PreviewWindow, you get an option for “newBoxBlurPreviewWindow()”. This new class is not abstract, but rather extends the abstract class, and so wecan create instances of it. (Don’t create an instance at this point — we just wanted you to see that you can.)

Let’s now implement the details for our BoxBlurPreviewWindow class.

• Start by declaring four private final instance variables: one each for the minimum, maximum, andinitial slider values (0, 10, and 3, respectively), and one for the text field width (here, 1).

• After the super call in your constructor, try constructing the JSlider inherited from PreviewWindow.The name of the instance variable in PreviewWindow is slider, so we should be able to reference itdirectly in our own constructor (without declaring a JSlider in this class!), similar to the following:

slider = new JSlider( SLIDER_MIN, SLIDER_MAX, SLIDER_INITIAL);

Add this code and try compiling the class. You will receive an error that “slider has private access inPreviewWindow”. Look back at the PreviewWindow class — slider is indeed declared as private. Youare beginning to see the implications of declaring things as public or private. We can’t declare thevariable as private because an inheriting class cannot access the variable. We shouldn’t declare thevariable as public because any class would be able to access (and modify!) the variable. The keywordwe want to use here is protected — this will allow inheriting classes direct access to the variable whileprohibiting direct access by all other classes.

So in PreviewWindow, change the instance variables from private to protected. Now compileBoxBlurPreviewWindow again — no errors this time.

• Finish the appropriate code in your constructor to make your box blur preview window look likethat on page 2. Create a new local JPanel, adding the inherited slider, new (local) JLabel’s, and

4

Page 5: inheritance - University of Richmonddszajda/classes/...— these differences will be handled when we create new (extending) classes for each preview. Now in the BlueJ main window,

the inherited text field (you still need to construct it) to the panel, and then adding the panel tothe BorderLayout.SOUTH part of the content pane. (You will need to import java.awt.* for theBorderLayout.) Finally, invoke this.sliderChanged() at the end of your constructor.

Test your class directly in the main BlueJ window, passing null as the argument to your constructor.No image will be displayed, but the buttons, labels, slider, and text field should look similar to thefigures shown on page 2. Test the cancel button to make sure the window goes away. (The acceptbutton will do nothing at this point.)

• Construct a skeleton method for the box blur algorithm:

public void boxBlur()

{

}

• Finish the code for the sliderChanged() and textEntered() methods. When the slider is changed,you should set the text in your text field to be the current value of the slider, and then call yourboxBlur() method (which for now does nothing).

When text is entered in the text field, you should set the value of the slider to the value entered inthe text field (refer to the Java API), have the text field request focus and select all, and then call yourboxBlur() method. To set the value of the slider, you will need to convert the text field entry fromString to an int. To do so, use Integer.parseInt( textField.getText() ).

Test your BoxBlurPreviewWindow directly in the BlueJ main window (passing null to the constructor).If you move the slider, the text field should update; if you enter a value into the text field, the slidershould update.

Now let’s take care of the actual box blur algorithm. Again, think ahead — we want this blur algorithmto work on either grayscale or color images. We know from above that if we apply an algorithm toeach of the red, green, and blue arrays, the algorithm will do the right thing regardless if the image isgrayscale or color. So, within our boxBlur() method, we want to call a separate method, passing inexactly one of the red/green/blue arrays, that will do the grunt work of blurring the given array.

To do this, first create a new private method named boxBlurArray(). The method will accept threearguments: a 2D integer array to be blurred, the width of that array, and the height of that array. Themethod will return a 2D integer array.

As discussed in lecture yesterday, to accomplish a box blur you will need two copies of the image’sarray: the original and (what will be) the blurred copy. The original array will be provided as aparameter. You will need to construct the blurred copy as a new (local) 2D integer array of the samewidth and height as the provided original array.

Then, you need to walk through the image’s original array pixel-by-pixel (again, use double-nested forloops). For each pixel, you want to compute the average of that pixel and its eight nearest neighbors(i.e., all the pixels in a 3 × 3 box centered on that pixel). This average will then become the corre-sponding value in the blurred copy array. Recall from class that the code necessary to compute theaverage for a pixel at (row, col) is similar to the following:

int sumOfPixels = 0;

int pixelsInBox = 0;

for ( int boxRow = row - 1; boxRow <= row + 1; boxRow++ )

{

for ( int boxCol = col - 1; boxCol <= col + 1; boxCol++ )

{

5

Page 6: inheritance - University of Richmonddszajda/classes/...— these differences will be handled when we create new (extending) classes for each preview. Now in the BlueJ main window,

sumOfPixels = sumOfPixels + originalArray[boxCol][boxRow];

pixelsInBox++;

}

}

blurredArray[col][row] = (int) (sumOfPixels / (double) pixelsInBox);

Finally, return the blurred array.

There is one problem with the code above. The line that computes the sum of pixels will cause anindex-out-of-bounds exception whenever the box is not completely inside the image. (Think aboutwhat happens when row and col are both 0. Then boxRow and boxCol both start at -1, which are notvalid indices into the array.) This can be fixed by “wrapping” pixels outside the array back to the otherside — e.g., the pixel at row 0 column -1 should wrap back to be the pixel at row 0 column (width -1). This is accomplished using some simple modular arithmetic. Just add the image width (height) toboxCol (boxRow) — to ensure a positive result — and then take the remainder when dividing by thewidth (height). In other words, instead of the above you should add to sumPixels the following:

originalArray[(boxCol + width) % width][(boxRow + height) % height]

Now back in the boxBlur() method, create local copies of each of the red/green/blue pixel arrays(using the inherited originalImage instance variable), and overwrite each of those arrays in turn bythe blurred array returned from a call to boxBlurArray (passing in the corresponding color array. Thenoverwrite the inherited instance variable currentImage by constructing a new SImage using the threeblurred color arrays and set the icon of the (inherited) JLabel to be this current image.

In order to test your algorithm, add to the actionPerformed method in your PhotoShop class theappropriate code to construct a new BoxBlurPreviewWindow instance. Then compile, create a newinstance of PhotoShop, and test your program using various images. (At this point, the slider will notaffect the blurring — for now you should see only slight blurring of the images you load.)

There are still two issues we need to address with box blurring:

1. making the slider affect the blurring;

2. making the accept button change the image in the main window.

To address the first issue, change the for loops that compute the walk through the box. We initiallyassumed that the radius of the box was 1, i.e., we computed the average by considering pixels onlywithin 1 of the center pixel. Instead, we want the radius of the box to be determined by the currentslider value. Change the for loops so that the average considers all pixels within radius of the centerpixel, where radius is the current value of the slider.

To address the second issue, we need a way to update the SImage back in the main PhotoShop window.One way to do this would be to include in our PreviewWindow class a reference back to the PhotoShop

class, construct a new method in PreviewWindow by which we can update its displayed image, andthen invoke that method if the user presses the accept button.

To implement this approach, do the following:

– in the PreviewWindow class, add a new private instance variable of type PhotoShop;

– change the constructor for PreviewWindow to accept a parameter of type PhotoShop, and in theconstructor set the new instance variable to this parameter;

– in the BoxBlurPreviewWindow class, change the constructor to also accept a PhotoShop parameter(matching the constructor for PreviewWindow), and in the constructor change the call to super()

to pass this parameter to the PreviewWindow constructor;

6

Page 7: inheritance - University of Richmonddszajda/classes/...— these differences will be handled when we create new (extending) classes for each preview. Now in the BlueJ main window,

– in the PhotoShop class, create a new public method named updateImage which accepts anSImage as a parameter, updates the SImage instance variable in that class using the parameter,and then sets the icon of the JLabel instance variable;

– in the buttonClicked() method in the PreviewWindow class, within the body of the if statementcorresponding to the accept button being pressed, invoke the updateImage() method you createdabove (you will use the PhotoShop instance variable you created in the first step) passing in thecurrentImage instance variable (i.e., the currently held blurred version of the image);

– in the actionPerformed() method in the PhotoShop class, update the statement that createda new instance of BoxBlurPreviewWindow to pass to its constructor a reference to the currentPhotoShop object — use the keyword this.

Carefully test your program to make sure you can use the slider to change the blurred image in thepreview window, and test to ensure that the accept and cancel buttons do the right thing.

Contrast Stretch: (Extra Credit) Creating the contrast stretch preview window will be much easier.

• Create a new class named ContrastStretchPreviewWindow, delete the default BlueJ code, copy andpaste the code from BoxBlurPreviewWindow, and change all occurrences of BoxBlur and boxBlur toContrastStretch and contrastStretch respectively.

• Change the text for the slider label to match that shown on page 2, and set the min, max, and initialvalue constants to 0, 255, and 128 respectively.

• Now all we need to do is change the code in the method contrastStretchArray() to do the rightthing. Recall from lecture yesterday that the approach here is to construct a histogram of the levelscurrently in the image, and then stretch those levels appropriately. We will stretch the levels basedon the amount of contrast selected by the user — 0 being no contrast (all gray); 255 being the mostcontrast possible under our approach.

1. First, create a histogram of the pixel values. There are 256 possible values, so create a 1D arrayof 256 possible values:

int[] histogram = new int[256];

2. Now walk through the original image’s pixel array (use double-nested for loops) and update thehistogram by adding one to the location in the histogram associated with the level of the currentpixel, using a statement like the following:

histogram[ pixels[col][row] ]++;

3. Then construct a cumulative histogram. This involves creating a new 1D array of 256 possiblevalues and setting each element in the new array to be the sum of all the histogram values up tothat point, e.g.,:

int[] cumulative = new int[256];

cumulative[0] = histogram[0];

for ( int i = 1; i < 256; i++ )

{

// cumulative value at point i is previous cumulative

// plus the current histogram value

cumulative[i] = cumulative[i - 1] + histogram[i];

}

7

Page 8: inheritance - University of Richmonddszajda/classes/...— these differences will be handled when we create new (extending) classes for each preview. Now in the BlueJ main window,

4. Now construct a stretched cumulative histogram. This involves creating a new 1D array of 256possible values. Then walk through the entire array and set each location using an equationsimilar to the following:

stretched[i] =

(int) Math.round( (cumulative[i] / (double)(width*height) * sliderValue)

+ ((255 - sliderValue) / 2.0) );

5. Finally, now step through the 2D pixel array (use doubly-nested for loops), setting each pixel’slevel according to the new stretched level stored in the stretched array, e.g.,

pixels[col][row] = stretched[ pixels[col][row] ];

and then return the updated pixel array.

Test your implementation carefully using various images.

Submission: Submit your lab in the usual process: send an email, with your zipped Lab 8 file as anattachment, to the appropriate email address for Lab 8.

8