Image processor (Java)

From LiteratePrograms

Jump to: navigation, search

Contents

Image Processor

This is a java program that performs graphical operations on an image. These operations are:

1)Reset the image

2)Invert the image colours

3)Gamma correction

4)Applying custom filters to the image

5)Convolution

6)Blue Fade

Using a GUI, users can select any image they wish to use.

For this program to work, users should save the image below to the directory that the source code is saved in.

Image:Raytrace.jpg

This is a good program to learn java with as it introduces how to produce graphical user interfaces. It also demonstrates how the extensive java API can be implemented to the programmers benefit.

Defining the class

It is essential to define a class at the beginning of a java program, listing all the library classes that the program will use and defining any initial variables or fields. The class definition is below:

<<Defining the class>>=
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import javax.swing.*;
import java.awt.event.*;
import java.util.ArrayList;
import javax.swing.table.DefaultTableModel;

public class ImageProcessor extends JFrame {

    JButton select_image_button, invert_button, gamma_button, 
convolve_button, reset_button,    slow_gamma_button, fast_gamma_button;
    JButton box_3x3_button, box_5x5_button, box_6x4_button, 
gaussian_3x3_button, high_3x3_button, select_file_button;
    JButton custom_filter_button, select_size_button, execute_button;
    JTextField gamma_value_height, width_field;
    JTextField[][] filter_array;
    JLabel info_label, height_label, width_label, gamma_label, image_icon;
    BufferedImage image, orig_image;
    JFileChooser file_chooser;
    JTable filter_table;
    Container container;
    JPanel control_container, menu_container, option_container, 
top_container, bottom_container;
    int height = 1, width = 1;

    private final String INITIAL_IMAGE_FILE = "raytrace.jpg";

    class body
}
<<class body>>=
Constructor
Event Handler
Return Pointer to Array
Reset
Invert
Slow Gamma
Fast Gamma
File Filters
Creating Custom Filter
Convolution
Blue fade
Main

Because this program creates a graphical user interface (GUI), it requires a lot of import statements to import and use java library classes in its implementation. What each class does and how their characteristics are described below:

java.awt.*- Contains all of the classes for creating user interfaces and for painting graphics and images.

java.awt.image- An abstract class, image is the superclass of all classes that represent graphical images.

java.io.*- Used for system input and output through data streams, serialization and the file system.

javax.imageio.*- Provides a pluggable architecture for working with images stored in files and accessed across the network.

javax.swing.*- Provides a set of lightweight GUI components that, to the maximum degree possible, work the same on all platforms.

java.awt.event.*- Provides interfaces and classes for dealing with different types of events created by AWT components.

java.util.ArrayList- Provides access to a library class which can store an arbitrary number of elements, with each element being another object.

javax.swing.table.DefaultTableModel- The TableModel interface specifies the methods the JTable will use to interrogate a tabular data model, this library class is an implementation of TableModel which uses a Vector of Vectors to store the cell value objects.

When naming a class it is important to name it something that identifies what it does or how it behaves. In this case the class is called ImageProcessor because it processes images in a certain way.

Notice the code extends JFrame. This indicates ImageProcessor is a subclass of the library class Jframe. Essentially, this means ImageProcessor is a Jframe. It inherits all the methods and behaviour of Jframe but adds some of its own functionality, which will be explained, along with the methods that provide this functionality.

The body of the class definition is taken up by defining the various components that are used to create the GUI. These components are described below, organised in groups of their component type.

Component Type: JButton A button component that can cause an event to occur when the user clicks on it.

Component Names:The select_image_button allows users to select an image of their choice from a certain location. The invert_button inverts the values of the image. The gamma_button allows the user to perform gamma correction on the image. The convolve_button allows users to apply convolution to the image.The reset_button resets the image to its original state. The slow_gamma_button performs gamma correction at a slow rate. The fast_gamma_button performs gamma correction at a fast rate. The box_3x3_button allows the user to apply a 3x3 blur filter to the image. The box_5x5_button allows the user to apply a 5x5 blur filter to the image. The box_6x4_button button allows the user to apply a 6x4 blur filter to the image. The gaussian_3x3_button button allows the user to apply a 3x3 Gaussian blur to the image. The high_3x3_button button applies a high pass filter to the image. The select_file_button allows users to select a file of their choice. The filter_button allows users to choose a filter of their choice. When the user wishes to use a filter of their choice they must select a filter size. The select_size_button allows them to do this. Once they have selected a filter size and input values, the execute_button applies the filter.

Component Type: JTextField. This is a text area on the screen that users can input values into.

Component Names:The gamma_value_height field allows users to enter a value to be used in the gamma correction, as does width_field. Note that the filter_array is of type JTextField [] []. This means it is a two dimensional array. The purpose of this is to store information that will become apparant later in the literate program.

Component Type: JLabel. This is used to display certain information on the GUI.

Component Names: info_label, height_label, width_label, gamma_label, image_icon

Component Type: BufferedImage. This is used to display images in the GUI.

Component Names: image is used to represent the image that the operations are going to be performed on. When the reset operation is performed orig_image is used to return the image to its original form.

Component Type: JFileChooser. This is a box that allows the user to select a file from a location their computer.

Component Name: file_chooser is used when the user wishes to select a file.

Component Type: JTable. This is a table that can be used in the GUI.

Component Name: filter_table. This is used when the user wishes to apply a unique filter to the image.

Component Type: Container. This is used to contain all the components that have been specified for the GUI.

Component Name: container. When the GUI has been created this will be used to manage their layout.

Component Type: JPanel. This is used when a number of components need to be grouped together.

Component Name: control_container, menu_container, option_container, top_container, bottom_container

The height and width of the filter are also defined and initialised to 1 in the body of the class definition.

Constructor

After defining all the components and fields that are going to be used in the program we must create instances of them so they can be used. This is achieved in the constructor of the class.

<<Constructor>>=
public void ImageProcessor() throws IOException {

        container = getContentPane();
        container.setLayout(new BorderLayout());

        File image_file = new File(INITIAL_IMAGE_FILE);
        if (image_file.exists())
        {
           image = ImageIO.read(image_file);
           orig_image = ImageIO.read(image_file);
           image_icon = new JLabel(new ImageIcon(image)); 
        } 
        else
        {
           image_icon=new JLabel(new ImageIcon());
        }
        container.add(image_icon, BorderLayout.CENTER);

        control_container = new JPanel(new BorderLayout());
        container.add(control_container, BorderLayout.SOUTH);

        info_label = new JLabel("Ready");
        control_container.add(info_label, BorderLayout.NORTH);

        menu_container = new JPanel(new GridLayout(5, 1));
        control_container.add(menu_container, BorderLayout.WEST);

        select_image_button = new JButton("Select Image");
        menu_container.add(select_image_button);

        invert_button = new JButton("Invert");
        menu_container.add(invert_button);

        gamma_button = new JButton("Gamma");
        menu_container.add(gamma_button);

        convolve_button = new JButton("Convolve");
        menu_container.add(convolve_button);

        reset_button = new JButton("Reset");
        menu_container.add(reset_button);

        option_container = new JPanel(new BorderLayout());
        control_container.add(option_container, BorderLayout.CENTER);

        top_container = new JPanel(new FlowLayout());
        option_container.add(top_container, BorderLayout.NORTH);

        gamma_value_height = new JTextField(6);

        gamma_label = new JLabel("Gamma Value:");

        height_label = new JLabel("Height:");

        slow_gamma_button = new JButton("Slow Gamma");

        fast_gamma_button = new JButton("Fast Gamma");

        box_3x3_button = new JButton("3x3 Box");

        box_5x5_button = new JButton("5x5 Box");

        box_6x4_button = new JButton("6x4 Box");

        gaussian_3x3_button = new JButton("3x3 Gaussian Blur");

        high_3x3_button = new JButton("3x3 High Pass");

        select_file_button = new JButton("Select File");

        custom_filter_button = new JButton("Custom Filter");

        width_field = new JTextField(6);

        width_label = new JLabel("Width:");

        select_size_button = new JButton("Select Size");

        execute_button = new JButton("Execute");

        bottom_container = new JPanel(new FlowLayout());
        option_container.add(bottom_container, BorderLayout.CENTER);

        file_chooser = new JFileChooser("Image Processor");

        GUIEventHandler handler = new GUIEventHandler();

        select_image_button.addActionListener(handler);
        invert_button.addActionListener(handler);
        gamma_button.addActionListener(handler);
        convolve_button.addActionListener(handler);
        reset_button.addActionListener(handler);
        slow_gamma_button.addActionListener(handler);
        fast_gamma_button.addActionListener(handler);
        box_3x3_button.addActionListener(handler);
        box_5x5_button.addActionListener(handler);
        box_6x4_button.addActionListener(handler);
        gaussian_3x3_button.addActionListener(handler);
        high_3x3_button.addActionListener(handler);
        select_file_button.addActionListener(handler);
        custom_filter_button.addActionListener(handler);
        select_size_button.addActionListener(handler);
        execute_button.addActionListener(handler);

        pack();
        setLocationRelativeTo(null);
        setVisible(true);
        top_container.setVisible(false);
        bottom_container.setVisible(false);
    }

The most vital component of the GUI is the image that will be altered with the methods of the class. The file raytrace.jpg is passed to a field called image_file at the start of the constructor. This is then passed to the BufferedImage image using the read method of the ImageIO library class. A copy is also created using the same method. This is called orig_image and is used when we need to reset the image.

After the image has been read the GUI needs to be set up. To begin with, the container needs to be initialised with the getContentPane method. The GUI also needs a layout, which is achieved by using the setLayout method. The BorderLayout is used for this GUI.

The first element to be put in the container is the image. This is passed as a JLabel and is called image_icon. After the image has been passed, it needs to be placed in the container using the add method of the awt library class. To place it in a position that is desired, the BorderLayout.CENTER is used. This tells the compiler to place the image in the centre of the container.

To have a user friendly interface the components of the container should be properly laid out. This can be achieved by separating the container into separate containers and applying individual layouts to each of them. The first of the separate containers is the control_container. This is a JPanel and has a BorderLayout. This is then added to the container using the add method of the awt library class. It is placed in the bottom of the container by using the code BorderLayout.SOUTH. The control container will hold the menu container, option container and information field.

The GUI should also have a label that indicates its status. This is called info_label, which is a JLabel and is set to "Ready". The label is added to the control_container using the add method of the Container class. It is placed at the top of control_container using the code BorderLayout.NORTH. It is important to say that this label is not placed at the top of the GUI, but at the top of the bottom container. This example indicates the importance of using more than one container to achieve a user friendly interface.

Another container is required for the buttons that will be used in program. This is called the menu_container. Again, the container is a JPanel but it adopts the GridLayout layout. The menu_container is added to the control_container using the add method of the awt library class. It is placed to the left in the control_container by the code BorderLayout.WEST.

The buttons select_image_button, invert_button, gamma_button, convolve_button and reset_button are then added to the menu_container using the add method of the awt library class. Each button is a JButton.

There is a need for another container that holds different options for the user depending on what buttons have been clicked, for example, if the custom_filter_button is clicked there needs to be an empty filter displayed. This container is a JPanel and is called option_container. It is added to the control_container using the add method of the awt library class. This container adopts the BorderLayout and is placed in the middle of the control_container using the CENTRE code.

Inside the option_container are two containers. The first to be intialised is the top_container. Again, this is a JPanel and adopts the FlowLayout layout. It is placed in the top of the option_container with the code BorderLayout.NORTH.

The components that could be used in the top_container, depending on the methods that are called, are then initialised. These are gamma_value_height and width_field which are JTextField components. gamma_label, width_label and height_label are initialised, which are JLabel components. There are also a number of JButton components that could be used. These are slow_gamma_button, fast_gamma_button, box_3x3_button, box_5x5_button, box_6x4_button, gaussian_3x3_button, high_3x3_button, select_file_button, custom_filter_button, select_size_button and execute_button.

Another container is required for the filter table that could be used if the custom filter method is called. This is a JPanel and is called bottom_container. It adopts the FlowLayout layout and is added to the option_container and placed in the middle of it with the code BorderLayout.CENTER.

If the user wishes to select their own image they need to use aJFileChooser. This is initialised in the constructor and is called file_chooser. It must have a directory for where the files come from, which is passed as a parameter when it is initialised. Note that this directory is simply where the file chooser opens. In this case the directory is E:\\ICCT\\Semester 4\\CS-217 Computer Graphics 1\\Assignment1\\Example\\. This directory should be changed to a more suitable directory if the program is going to be run.

For the GUI to interact with the user there needs to be event handlers. These essentially allow the component to respond to a users action. The event handler in this case is called handler and is a GUIEventHandler, which is defined later in the program. This event handler must be 'placed' behind a button so they can react to the user clicking on them, for example. This is achieved by calling the addActionListener method of the GUIEventHandler and passing handler as a parameter to each button, such as the select_image_button button, for example.

Now that all the components are created, initialised and event handlers have been set, the GUI needs to be displayed. This is achieved by using methods of the swing library class. The first method that is used is pack. This packs all the components together into an object. The next method, setLocationRelativeTo, with a parameter null allows the components to be placed with no relation to other objects. To allow the user to see the components the setVisible method is called, which is set to true by the passing of a parameter. top_container and bottom_container use the same method, but the parameter false is passed, meaning they will not be displayed at this time.

Event Handler

Now that each button has been initialised with an event handler it is important the event handler is defined. The method below defines the event handler.

<<Event Handler>>=
private class GUIEventHandler implements ActionListener {
        public void actionPerformed(ActionEvent event) {
            info_label.setText("Ready");
            if (event.getSource()==select_image_button) {
                Container parent = select_image_button.getParent();
                file_chooser.showOpenDialog(parent);
                try {
                    File image_file = file_chooser.getSelectedFile();
                    if (!image_file.getName().endsWith(".jpg") && 
!image_file.getName().endsWith(".jpeg")) {
                        throw new IOException();
                    }
                    image = ImageIO.read(image_file);
                    orig_image = ImageIO.read(image_file);
                    image_icon.setIcon(new ImageIcon(image));
                }
                catch (NullPointerException e) {
                    info_label.setText("ERROR: No File Selected");
                }
                catch (IOException e) {
                    info_label.setText("ERROR: File Must Be Of Type .jpg Or .jpeg");
                }
            }
            invert button event handler
            gamma button event handler
            convolve button event handler
            reset button event handler
            slow gamma button event handler
            fast gamma button event handler
            box 3x3 button event handler
            box 5x5 button event handler
            box 6x4 button event handler
            gaussian 3x3 button event handler
            high pass 3x3 button event handler
            select file button event handler
            custom filter button event handler
            select size button event handler
            execute button event handler
            }
        }
            

Notice that the GUIEventHandler is a private class. This means that it cannot be seen by other classes. It also implements ActionListener, which means it takes some of the behaviour from the ActionListener and adds some functionality of its own.

The first and only method of the GUIEventHandler class is called actionPerformed. The method signature shows it is of return type void, which means it returns nothing. The method takes a parameterevent of type ActionEvent, which is part of the awt library class.

The method body begins by setting the value of info_label to ready. This is achieved by using the setText method of the swing library class.

The remainder of the method body involves a number of checks to see which button has been clicked. An if statement is used to check what button is clicked by calling the getSource method of the ActionEvent library class. The result is then compared to each button, if the result matches the button name, the following statement is executed. If the result doesnt match the button name, the next else if statement is executed to compare a different button name. This process is repeated everytime a button is pressed.

The first button to be checked is the select_image_button button. If the result of the getSource method call is equal to the select_image_button the getParent method is called. This is part of the awt library class which gets the parent of the button, which in this case in the container it is in.

The next statement tries to open the file_chooser dialog box by calling the showOpenDialog method of the swing library class. The parent container is passed as a parameter to this method call. Once the dialog box has been opened, the method tries to get the file that the user selects. This utilises a try-catch block. The first part of this is a try section, which basically attempts something that could result in an exception, which is undesirable and needs to be caught in a catch statement. In this case the try statement attempts to get the file that has been selected by the user. This is achieved by calling the getSelectedFile method of the swing library class and passing the result to the variable image_file. The user then has the opportunity to select any file type they want, which could cause problems as the program can only deal with jpg image types. This means the program should perform a check on the file name. This check is performed in an if statement that calls the methods getName of the swing library class and endsWith of the lang library class to check if the extension of the file chosen is jpg or jpeg. If this check returns false an IOException is thrown. This is caught in a catch block which calls the setText method of the swing library class to change the value of info_label so it displays an error message informing the user that the file type must be of jpg or jpeg.

If the check returns a true result, the image is opened and read into the image variable, which is a BufferedImage. The method read of the ImageIO library class allows this to occur. It takes the image_file field as a parameter and stores it in the BufferedImage variable. There may be a time where the image needs to be reset, so the same procedure is repeated and the result is stored in the orig_image variable so there is a copy that can be used to reset the image. The image that is displayed in the GUI then needs to be updated, so the setIcon method is called, taking the image file as a parameter. This new image then replaces the image_icon variable, meaning the image displayed on the GUI is replaced.

<<invert button event handler>>=
else if (event.getSource()==invert_button) {
    image=Invert(image);
    image_icon.setIcon(new ImageIcon(image));
}
gamma button event handler

If the result of getSource method doesn't match the value of theselect_image_button, the result is checked against the value of the invert_button. If the results are equal, the Invert method of the ImageProcessor is called to manipulate the image. What this method does will be explained later in the literate program. The result is then stored in the image variable. The setIcon method of the swing is used to update the GUI componentimage_icon.

<<gamma button event handler>>=
else if (event.getSource()==gamma_button) {
    top_container.setVisible(false);
    bottom_container.setVisible(false);
    top_container.removeAll();
    bottom_container.removeAll();
    top_container.add(gamma_label);
    top_container.add(gamma_value_height);
    gamma_value_height.setText("1");
    top_container.add(slow_gamma_button);
    top_container.add(fast_gamma_button);
    top_container.setVisible(true);
}
convolve button event handler

If the previous check was unsuccessful, the gamma_button value is checked. If the result of gamma_button matches the result of the getSource method the GUI is re-organised to allow for the gamma options. To add new icons to a container, it needs to be emptied and the new components need to be added to it once its empty. The top_container and bottom_container are made invisible by passing a false parameter to the setVisible method of the swing library class. The components of each container are removed by calling the removeAll method of the swing library class. Once the containers are empty, new components are added to them using the add method of the swing library class. These components are gamma_label and gamma_value_height. The setText method of the awt library class is called to set the value of gamma_value_height to 1. The components slow_gamma_button and fast_gamma_button are then added to top_container by calling the add method of the swing library class. All the new components of the top_container are displayed by calling the setVisible method of the swing library class and passing a true parameter to it.

<<convolve button event handler>>=
else if (event.getSource()==convolve_button) {
    top_container.setVisible(false);
    bottom_container.setVisible(false);
    top_container.removeAll();
    bottom_container.removeAll();
    top_container.add(box_3x3_button);
    top_container.add(box_5x5_button);
    top_container.add(box_6x4_button);
    top_container.add(gaussian_3x3_button);
    top_container.add(high_3x3_button);
    bottom_container.add(select_file_button);
    bottom_container.add(custom_filter_button);
    top_container.setVisible(true);
    bottom_container.setVisible(true);
}
reset button event handler

If the previous check was unsuccessful, the result of getSource is checked against the value of convolve_button. If these are equal, the GUI needs to be re-organised to accommodate the convolution components. The top_container and bottom_container are made invisible and emptied in the same manor as described above. The buttons are then added to the top_container by calling the add method of the swing library class. These buttons are box_3x3_button, box_5x5_button, box_6x4_button, gaussian_3x3_button, high_3x3_button. The same is repeated for bottom_container, adding the select_file_button and the customer_filter_button buttons. The new components are then displayed by calling the setVisible method and passing true as a parameter.

<<reset button event handler>>=
else if (event.getSource()==reset_button) {
    image=Reset(image);
    image_icon.setIcon(new ImageIcon(image));
}
slow gamma button event handler

The result of the method call getSource is then checked against the value of reset_button, providing the previous check was unsuccessful. If the result is equal to the value of reset_button, the Reset method of the ImageProcessor class is then called. What this method does is explained later in the literate program. The result of this method is then stored in the image variable. The new value of image is then passed as a parameter to the setIcon of the swing library class, which updates the image_icon GUI component.

<<slow gamma button event handler>>=
else if (event.getSource()==slow_gamma_button) {
    image=SlowGamma(image);
    image_icon.setIcon(new ImageIcon(image));
}
fast gamma button event handler

If the results of the previous comparison are equal, the next button to be tested against the result of getSource is the slow_gamma_button. If the values match, the SlowGamma method of the ImageProcessor class is called. This takes the image variable as a parameter, processes it in some way and returns a new version of the image. The processing of this method will be described later in the literate program. The new result is stored in the image variable, which is then passed as a parameter to the setIcon method of the swing library class, which updates the image_icon GUI component.

<<fast gamma button event handler>>=
else if (event.getSource()==fast_gamma_button) {
    image=FastGamma(image);
    image_icon.setIcon(new ImageIcon(image));
}
box 3x3 button event handler

If the result of getSource doesn't match the value of slow_gamma_button, it is compared with the value of fast_gamma_button. If the result matches the value of the button, the FastGamma method is called. This method is part of the ImageProcessor class and it takes the image variable as a parameter. The method does some processing, which is described later in the literate program and returns the new value of the image variable. This new value is stored in theimage variable, which is passed as a parameter to the setIcon method of the swing library class. This is used to update the image_icon GUI component.

<<box 3x3 button event handler>>=
else if (event.getSource()==box_3x3_button) {
    height = 3;
    width = 3;
    int[][] filter = new int[height][width];
    for (int j = 0; j < height; j ++) {
       for (int i = 0; i < width; i++) {
          filter[j][i] = 1;
       }
     }
     image=Convolve(image, filter, width, height);
     image_icon.setIcon(new ImageIcon(image));
}
<<box 5x5 button event handler>>=

If the previous check was unsuccessful, the next value to be checked is the value of the box_3x3_button. If the result of getSource is equal to the value, the variables height and width are set to 3. This represents the size of the filter that is going to be used, ie 3x3. These values are used to represent elements of an array of type int. This array is called filter. A for loop is created, which uses loop variables j and i. These loop variables are used to iterate through the array until the size of height and weight are reached, in this case 3 for both variables. After the initialising and defining the array, the method Convolve is called. The fields image, filter, width and height are passed as parameters. The method applies some processing that will be explained later in the literate program and updates the image variable with the new result. The updated image variable is then passed as a parameter when the setIcon method of the swing library class is called to update the image_icon GUI component.

<<box 5x5 button event handler>>=
else if (event.getSource()==box_5x5_button) {
    height = 5;
    width = 5;
    int[][] filter = new int[height][width];
    for (int j = 0; j < height; j ++) {
       for (int i = 0; i < width; i++) {
          filter[j][i] = 1;
       }
    }
    image=Convolve(image, filter, width, height);
    image_icon.setIcon(new ImageIcon(image));
}
box 6x4 button event handler

If the previous check was unsuccessful, the result of getSource is checked against the value of box_5x5_button. If the values are equal, the variables height and width are set to the value of 5. The new values are then used to set the size of the array filter. A for loop is created, which uses loop variables j and i. These loop variables are used to iterate through the array until the size of height and weight are reached, in this case 5 for both variables. After the initialising and defining the array, the method Convolve is called. The fields image, filter, width, height are passed as parameters. The method applies some processing that will be explained later in the literate program and updates the image variable with the new result. The updated image variable is then passed as a parameter when the setIcon method of the swing library class is called to update the image_icon GUI component.

<<box 6x4 button event handler>>=
else if (event.getSource()==box_6x4_button) {
    height = 6;
    width = 4;
    int[][] filter = new int[height][width];
    for (int j = 0; j < height; j ++) {
       for (int i = 0; i < width; i++) {
          filter[j][i] = 1;
       }
    }
    image=Convolve(image, filter, width, height);
    image_icon.setIcon(new ImageIcon(image));
}
gaussian 3x3 button event handler

The next button to be compared against the result of getSource is the box_6x4_button button. If the result matches the value, the value of height is set to 6 and the value of width is set to 4. The new values are then used to set the size of the array filter. A for loop is created, which uses loop variables j and i. These loop variables are used to iterate through the array until the size of height and weight are reached, in this case 6 and 4 for each variable, respectively. After the initialising and defining the array, the methodConvolve is called. The fields image, filter, width and height are passed as parameters. The method applies some processing that will be explained later in the literate program and updates the image variable with the new result. The updated image variable is then passed as a parameter when the setIcon method of the swing library class is called to update the image_icon GUI component.

<<gaussian 3x3 button event handler>>=
else if (event.getSource()==gaussian_3x3_button) {
    height = 3;
    width = 3;
    int[][] filter = new int[height][width];
    filter[0][0] = 1;
    filter[0][1] = 3;
    filter[0][2] = 1;
    filter[1][0] = 3;
    filter[1][1] = 9;
    filter[1][2] = 3;
    filter[2][0] = 1;
    filter[2][1] = 3;
    filter[2][2] = 1;
    image=Convolve(image, filter, width, height);
    image_icon.setIcon(new ImageIcon(image));
}
high pass 3x3 button event handler

If the previous check was unsuccessful, the result of the getSource method call is compared to the value of gaussian_3x3_button. If these values are equal, the value of height and width are set to the value of 3. Each element of the array is set to a certain value, in this case, the filter that is going to be applied to the image is a Gaussian 3x3 filter, so the array elements each represent one value in the filter ie 1,3,1,3,9,3,1,3,1. After the initialising and defining the array, the methodConvolve is called. The fields image, filter, width and height are passed as parameters. The method applies some processing that will be explained later in the literate program and updates the image variable with the new result. The updated image variable is then passed as a parameter when the setIcon method of the swing library class is called to update the image_icon GUI component.

<<high pass 3x3 button event handler>>=
else if (event.getSource()==high_3x3_button) {
    height = 3;
    width = 3;
    int[][] filter = new int[height][width];
    filter[0][0] = -1;
    filter[0][1] = -1;
    filter[0][2] = -1;
    filter[1][0] = -1;
    filter[1][1] = 8;
    filter[1][2] = -1;
    filter[2][0] = -1;
    filter[2][1] = -1;
    filter[2][2] = -1;
    image=Convolve(image, filter, width, height);
    image_icon.setIcon(new ImageIcon(image));
}
<<select file button event handler>>=

If the check of getSource result and the previous button was unsuccessful, the result of the getSource method call is compared to the value of high_3x3_button. If these values are equal, the value of height and width are set to the value of 3. Each element of the array is set to a certain value, in this case, the filter that is going to be applied to the image is a high pass 3x3 filter, so the array elements each represent one value in the filter ie -1, -1, -1, -1, 8, -1, -1, -1, -1. After the initialising and defining the array, the method Convolve is called. The fields image, filter, width, height are passed as parameters. The method applies some processing that will be explained later in the literate program and updates the image variable with the new result. The updated image variable is then passed as a parameter when the setIcon method of the swing library class is called to update the image_icon GUI component.

<<select file button event handler>>=
else if (event.getSource()==select_file_button) {
    Container parent = select_file_button.getParent();
    file_chooser.showOpenDialog(parent);
    try {
       File filter_file = file_chooser.getSelectedFile();
       if (!filter_file.getName().endsWith(".filt")) {
          throw new IOException();
       }
       Object[] filter_info = FileFilterArray(filter_file);
       int[][] filter = (int[][])filter_info[0];
       int[] dimentions = (int[])filter_info[1];
       width = dimentions[0];
       height = dimentions[1];
       image=Convolve(image, filter, width, height);
       image_icon.setIcon(new ImageIcon(image));
    }
    catch (NullPointerException e) {
       info_label.setText("ERROR: No File Selected");
    }
    catch (IOException e) {
       info_label.setText("ERROR: File Must Be Of Type .filt");
    }
}
custom filter button event handler

The next button to be compared against the result of the getSource is select_file_button. If the values match, the parent container is obtained by calling the getParent method of the awt library class. The result is stored in a field parent which is of type Container. This field is passed as a parameter to the showOpenDialog method of the swing library class. This is called to open the file_chooser window. Once the file_chooser window is open a try block is used to attempt to get the selected filter, this is achieved by calling the getSelectedFile of the swing library class. The result of this method call is passed to the field filter_file, which is of type File. A check is then performed on the file type of filter_file by calling the methods getName and endsWith of the library class lang. If the result of these method calls isn't .filt then an IOException is thrown. This is dealt with in a catch block that calls the setText method of the awt library class to change the value of info_label to inform the user a .filt file is needed.

If the file is of the correct type, the FileFilterArray method of the image_processor class is called, passing the filter_file as a parameter. This method creates a filter from a file. The result of the method call is stored in a field called filter_info, which is of type ArrayList. Before passing the fields image, filter, width and height as parameters to the Convolve method, the values in the filter need to be separated from the dimensions of the filter. This is achieved by calling the get method of the ArrayList library class to retrieve the information of filter_info. The results are then cast using the (int[] []),to turn the information into type two dimensional array. This makes it compatible with the two dimensional array. The first element of filter_info is the values of the filter. These values are stored in the two dimensional array filter. The second element of the filter_info is the dimensions of the filter. These are stored in the one dimensional array dimentions. The values of the first two elements of dimentions are then stored in width and height, respectively.

Now that the values of the filter_info have been separated, they can be passed to the Convolve method of the ImageProcessor class, which provides some form of image processing that will be described later in the literate program. The result of this method is stored in the image field, which is then passed as a parameter to the setIcon of the swing library class. This method is used to update the GUI component image_icon to the new value of image.

<<custom filter button event handler>>=
else if (event.getSource()==custom_filter_button) {
    top_container.setVisible(false);
    bottom_container.setVisible(false);
    top_container.removeAll();
    bottom_container.removeAll();
    top_container.add(height_label);
    top_container.add(gamma_value_height);
    gamma_value_height.setText("1");
    top_container.add(width_label);
    top_container.add(width_field);
    width_field.setText("1");
    top_container.add(select_size_button);
    top_container.add(execute_button);
    execute_button.setEnabled(false);
    top_container.setVisible(true);
}
select size button event handler

The custom_filter_button is then checked against the result ofgetSource providing the previous results do not match. If these results are equal, the GUI needs to be reorganised to accommodate the custom filter. The setVisible function of the awt library class is called, with a false value as a parameter to make the top_container and bottom_container invisible. Their contents are then removed with a method call of removeAll, which is part of the awt library class. The new components are then added by calling the add method of the awt library class. Each component to be added is passed as a parameter to this method, these components are height_label, gamma_value_height, width_label, width_field, select_size_button and execute_button. The setText method of the awt is called to set the text of width_field and gamma_value_height to 1. The execute_button is disabled by calling the setEnabled method of the awtlibrary class and passing false as a parameter. Once all the components are added, the setVisible method is called, with true passed as a parameter. This displays the new components of the top_container on the GUI.

<<select size button event handler>>=
else if (event.getSource()==select_size_button) {
    bottom_container.setVisible(false);
    bottom_container.removeAll();
    try {
       height = Integer.parseInt(gamma_value_height.getText());
       width = Integer.parseInt(width_field.getText());
       if (height < 1 || width < 1) {
          throw new IllegalArgumentException();
       }
       filter_table = new JTable(height, width);
       bottom_container.add(filter_table);
       execute_button.setEnabled(true);
       bottom_container.setVisible(true);
    }
    catch (IllegalArgumentException e) {
       info_label.setText("ERROR: Filter Dimensions Must Be A Positive Integer");
    }
}
execute button event handler

If the previous results did not match, the result of the getSource method call is checked against the select_size_button button. If the result of getSource matches the button the bottom_container needs to be removed. This is achieved by firstly calling the setVisible method of the awt and passing false as a parameter. This is followed by calling the removeAll method of the awt class to empty the contents of bottom_container. Once the bottom_container has been removed, the values of gamma_value_height and width_field need to be stored in the height and width fields, respectively. This is achieved by calling the getText method of the awt library class to return the values of gamma_value_height and width_field. This returns a result that is of type String, but the fields height and width are of type int. In order to convert the result from String to int the code Integer.parseInt needs to be used. The results can then be stored in the fields height and width.

A try block is then used to check if the values of height and width are less than 1. If they are, an IllegalArgumentException is thrown. This is caught in a catch block that calls the setText method of the library class awt to set the text of info_label with a message informing the user that they must enter a positive number. If the values of height and width are equal to, or greater than 1, a table needs to be constructed using the values the user has entered. This is achieved by creating a JTable called filter_table, which takes height and width as parameters for its size. This is then added to bottom_container by calling the add method of the swing library class. The execute_button also needs to be usable, so the setEnabled method of the awt library class is called and true is passed to it. The new components of bottom_container are displayed by calling the setVisible method of the awt library class and passing true as a parameter.

<<execute button event handler>>=
else if (event.getSource()==execute_button) {
    int[][] filter = CustomFilterArray(height, width);
    image=Convolve(image, filter, width, height);
    image_icon.setIcon(new ImageIcon(image));
}

The final button to be checked against is the execute_button. If the value of this button matches the result of the getSource method call, the CustomFilterArray method of the imageProcessor class is called. This creates a filter using the values of height and width to created a two dimensional array called filter, which represents the filter. Once the array has been created, the Convolve method of the imageProcessor class is called which takes the parameters image, filter, height and width. What this method does is explained later in the literate program. The result of this method is stored in the field image, which is passed as a parameter to the setIcon of the awt library class. This method updates the image_icon GUI component.

Return pointer to array

Because the image data is going to be used repeatedly, it is a good idea to store the image data into an array of bytes. A pointer can be used to represent the image when any processing is performed on it. This allows faster access than getting the pixel values and performing the processing on each pixel. The method below returns a pointer to an array to allow this quick processing to be achieved.

<<Return Pointer to Array>>=
    public static byte[] GetImageData(BufferedImage image) {
        WritableRaster WR=image.getRaster();
        DataBuffer DB=WR.getDataBuffer();
        if (DB.getDataType() != DataBuffer.TYPE_BYTE) {
            throw new IllegalStateException("That's not of type byte");
        }
        return ((DataBufferByte) DB).getData();
    }

Notice the method signature is public. This means it is seen by other classes. The method is also static, which means it can only be called once. This method only needs to be called once, calling it more than once could return a pointer to the image that has had some processing performed on it. It is bad programming to have more than one instance of an object that will be used more than once in different ways. The method signature also indicates that the method body will eventually return an array of type byte. This method is called GetImageData, as it returns the image data in the form of a pointer to an array. To achieve this, the method takes image as a parameter, which is of type BufferedImage.

The method body creates a field of type WritableRaster called WR. This stores the result of the getRaster method call of the library class awt on the image field. The getDataBuffer method is then called on WR. This method is of the awt library class. The result of this method call is stored in a new field DB, which is a DataBuffer.

An if statement is used to check the data type of DB. This is achieved by calling the getDataType method of the awt library class. If the result is not of type byte an IllegalStateException is thrown, which informs the user that the image data type is not type byte. If the data type is of type byte a return statement is used to return the result of the awt library class method getData on the DB field. The result of this needs to be cast to the type DataBufferByte so it can be used in other methods.

Reset Image

Because the image may alter a number of times during the execution of the program, certain processing may not alter the image the way it is expected to. For example, after inverting the image, applying convolution would not perform the correct processing on the image as the values it is altering are not what they should be. Before a new operation is performed on the image, it should be reset. For this reason, the GUI for this program contains a reset button. This sets the image back to its original state after some processing has been performed on the image. The method below resets the image.

<<Reset>>=
    public BufferedImage Reset(BufferedImage image) {
        int w=image.getWidth(), h=image.getHeight();
        byte[] data = GetImageData(image);
        byte[] orig_data = GetImageData(orig_image);
        for (int j=0; j<h*w*3; j++) {
            data[j] = orig_data[j];
        }
        return image;
    }

The method resets the image, so it has been sensibly called Reset. It is of type BufferedImage meaning that eventually the method body will return an object of type BufferedImage. Because the method is going to be resetting the image variable, it is passed as a parameter.

The method body uses the image dimensions(width and height) and a for loop to reset the image, this loop requires two variables j and i. The image width is passed from the method call getWidth of the awt library class. This is stored in a field called w. The getHeight method of the awt library is called to pass the image height into a field h. All the variables described above are of type int.

A byte array is then created called data. This is used to store the result of the GetImageData method of the ImageProcessor class, which returns a pointer to the image.

Another byte array is also created, called orig_data. This is used to store the result of the GetImageData method of the ImageProccesor class, which returns a pointer to the orig_image. This is the image that is going to be used to reset the current image.

The for loop is then defined. The j variable is set to 0.

The loop body is executed while the value of j is less than the value of h*w*3. This is because the image dimensions and colour channel need to be mapped from a 1-dimensional array to the values of a 3-dimensional array. Each time the loop body is executed, j is increased by 1.

The loop body consists of a statement that sets the value of the pixel at the point j in the data array to the value of the same point in orig_data. This effectively replaces the current image with the original image. The reset image is then returned with the return statement.

Invert Image

A classic image processing function is the invert function. This inverts the pixel colour values of the image. This function is implemented in this class, and the method is described below.

<<Invert>>=
    public BufferedImage Invert(BufferedImage image) {
        int w=image.getWidth(), h=image.getHeight();
        byte[] data = GetImageData(image);
        for (int j=0; j<h; j++) {
            for (int i=0; i<w; i++) {
                for (int c=0; c<3; c++) {
                    data[c+3*i+3*j*w] = (byte)~data[c+3*i+3*j*w];
                }
            }
        }
        return image;
    }

This method inverts the image, so it has been called Invert to indicate what it achieves. It is of type BufferedImage, so it will eventually return an object of type BufferedImage. This method takes image as a parameter as it will be performing some form of processing on it.

Again, this method obtains the image width and height using the same means as the Reset method. These values are stored in the fields w and h, which are type int. The method also uses a for loop, which uses the variables i, j and c. These variables are defined at the start of the method body as type int.

The image data is then passed from the GetImageData method of the ImageProcessor class to the data field, which is an array of type byte.

A for loop is then used to iterate through each colour channel, row and column pixels and perform the inversion process on them. The first for loop initialises j to 0. While j is less than the image height h, the loop body is executed. Each time the loop body is executed, j is increased by 1. The second for loop initialises i to 0. While i is less than the image width w, the loop body is executed. Every time the loop body is executed, i is increased by 1. The final loop initialises c to 0, while c is less than 3 (because there are 3 colour channels red, green, and blue) the loop body is executed. Each time the loop body is executed, the value of c is increased by 1.

The loop body maps positions of pixel values of the rows columns and colour channels of a 3-d array to a 1-d array, but in the format that can be used to represent the new values, so they can be used in to the rows columns and colour channels of a 3-d array. This is achieved by multiplying c by 3, i by 3 and j by w. A simple procedure to "invert" a byte (i.e. map 0 to 255, 1 to 254, ... 255 to 0) is to use the bitwise NOT operator ~ on the byte. This works even though Java doesn't explicitly "unsigned" bytes. Then result needs to cast to int before being stored in the correct position.

The new value of image is then returned to the GUI can be updated.

Slow Gamma

Gamma correction is important when displaying images accurately on computer monitors. Gamma correction controls the overall brightness of an image. If gamma correction is wrong, images can look either very faded or very dark. Varying the amount of gamma correction changes not only the brightness of the image, but also the ratios of red to green to blue. Gamma correction is an important image processing technique and it can be achieved in a number of ways. The method below is a basic approach to gamma correction and does not perform the process at the quickest possible rate.

<<Slow Gamma>>=
public BufferedImage SlowGamma(BufferedImage image) {
        int w=image.getWidth(), h=image.getHeight();
        byte[] data = GetImageData(image);
        double gamma = 1;
        try {
            gamma = Double.parseDouble(gamma_value_height.getText());
            if (gamma<=0) {
                throw new IllegalArgumentException();
            }
        }
        catch (IllegalArgumentException e) {
            gamma = 1;
        }
        if (gamma==1) {
            info_label.setText("ERROR: Gamma Value Must Be A Number Greater than 0 Excluding 1 As It Has No Effect");
        }
        else {
            int i=w*h*3;
            for (int j=0; j<i; j++) {
                data[j] = (byte)(Math.pow((data[j]&0xFF)/255.0,gamma)*255);
            }
        }
        return image;
    }

The method signature shows that this method is called SlowGamma. This indicates that this method performs gamma correction at a slow rate. Again, this method is of return type BufferedImage and it takes image as a parameter.

The method body begins in the same manor as the previous two methods in the ImageProcessor class, storing the image height and width in the fields h and w, respectively. The loop variables i and j are also defined. The image data is also stored in the one dimensional array data in the same manner as the two previous methods.

The similarities of the two previous methods and this method then end. A new field gamma is created. This is of type double, which allows values of whole and decimal numbers to be stored in it. The gamma variable is initialised to 1.

A try block is then used to attempt to get the gamma value the user wishes to enter from the JTextArea gamma_value_height. This is achieved by calling the getText method of the awt library class. This will return a value that needs to be parsed into a double type as it will be stored into the gamma variable, which is of type double. This can be done by using the code Double.parseDouble. The value that the user has entered is then stored in the gamma variable.

An if statement is then used to check if the value that the user has entered, which is now stored in gamma, is less than or equal to 0. If it is, a new IllegalArgumentException is thrown. This is caught in a catch block that deals with the exception and sets the gamma variable back to 1 and the try block is again executed.

Another if statement is used to test whether the value that the user has input is equal to 1. If this is the case, the value of info_label is altered, informing the user that they must enter a value greater than 0, but excluding 1. If this is not the case, an else statement is used to execute the image processing code.

The loop body maps positions of pixel values in a 1-d array to the rows columns and colour channels of a 3-d array. For the time being, however, it is mapped to a 1-dimensional array which represents the pointer to image data.

The new image is then returned so it can be used to update image displayed in the GUI.

Fast Gamma

As mentioned above, the previous method is a slow method of gamma correction. The reason for it being slow is the image size is 634 x 455 and there are 3 colour channels. Multiplying these three values together means there is a total of 865410 calculations required to perform gamma correction on the image. The slow gamma correction method doesn't take into account the fact that there are only 256 possible values that a colour value can take. This means that it does the same 256 calculations 3381 times on each colour channel, which is a lot of redundancy. The method below implements a look up array where keys represent the old colour values and the values represent the new ones. This means only 256 calculations are required for each colour channel.

<<Fast Gamma>>=
    public BufferedImage FastGamma(BufferedImage image) {
        int w=image.getWidth(), h=image.getHeight();
        byte[] data = GetImageData(image);
        double gamma = 1;
        byte[] lookup = new byte[256];
        try {
            gamma = Double.parseDouble(gamma_value_height.getText());
            if (gamma<=0) {
                throw new IllegalArgumentException();
            }
        }
        catch (IllegalArgumentException e) {
                gamma = 1;
        }
        if (gamma==1) {
            info_label.setText("ERROR: Gamma Value Must Be A Number Greater than 0 Excluding 1 As It Has No Effect");
        }
        else {
            for (int k=0; k<256; k++) {
                lookup[k] = (byte)((Math.pow(k/255.0, gamma))*255);
            }
            int i=w*h*3;
            for (int j=0; j<i; j++) {
                data[j] = lookup[data[j]&0xFF];
            }
        }
        return image;
    }

The method is suitably named FastGamma and the signature shows it is again of return type BufferedImage, taking the image variable as a parameter.

The method body starts in much the same way as the SlowGamma method, getting image dimensions, defining loop variables i, j, k, storing the image data in the data array and initialising gamma to 1.

The look up array is then initialised, which will be used to reduce the redundancy explained above. The array is called lookup and is of type byte. This array is set to the size of 256, which represents the amount of possible values that a colour can have. Once the array has been initialised, a try block is created. This attempts to retain the gamma value that the user has entered into the JTextArea gamma_value_height by calling the getText method of the awt library class. The value needs to be stored into the gamma variable, which is of type double, therefore, the value needs to be converted into a double. This is achieved by using the code Double.parseDouble. The value is then stored in the gamma variable.

The value of gamma is then checked in an if statement. If its value is less than or equal to 0, a IllegalArgumentException is thrown. This is dealt with in a catch block that sets the value of gamma back to 1 and the value of gamma_value_height is checked again.

Another if statement is used to check if the value input by the user, which is now stored in gamma is 1. If it is, the GUI component info_label is changed to inform users that the gamma value must be any value greater than 0, excluding 1. If the value of gamma is not 1, an else statement is used to perform the image processing. The first stage of the fast gamma image processing technique is creating a lookup table for every possible value

A lookup table is constructed by calculating the new value for every value between 0 and 255 using the code Math.pow(k/255.0, gamma))*255. Once this table is created, the pixels in the image are compared to the corresponding values of the lookup table with the code lookup[data[j]&0xFF]. These new values are then stored in data[j]

The new values of image are then returned using a return statement. This new version of image is used to update the image_icon GUI component.

Using filters from files

To have a fully functioning image processor, it is a good idea to allow users to read a filter in from a file, instead of using filters that are hard coded into the program. The process of reading in a filter from a file is described below.

<<File Filters>>=
    // FIXME: this returning mechanism is really type-unsafe
    public Object[] FileFilterArray(File filter_file) {
        BufferedReader reader = null;
        ArrayList<String> lines = new ArrayList<String>();
        try {
            reader = new BufferedReader(new FileReader(filter_file));
            for (String line = reader.readLine(); line != null; line = 
reader.readLine()) {
                lines.add(line);
            }
        }

        catch (FileNotFoundException e) {
            info_label.setText("ERROR: File Not Found");
        }

        catch (IOException e) {
            info_label.setText("ERROR: Unable To Read File");
        }

        finally {
            if (reader != null) {
                try {
                    reader.close();
                }
                catch (IOException e) {
                    info_label.setText("ERROR: Unable To Close File");
                }
            }
        }
        int no_lines = lines.size();
        int no_values = lines.get(0).split(",").length;
        int[][] filter = new int[no_lines][no_values];
        String[][] split_lines = new String[no_lines][no_values];
        for (int i = 0; i < no_lines; i++) {
            split_lines[i] = lines.get(i).split(",");
            try {
                if (split_lines[i].length != no_values ) {
                    throw new Exception();
                }
            }
            catch (Exception e) {
                info_label.setText("ERROR: Filter File Not Valid");
            }
        }
        try {
            for (int j = 0; j < no_lines; j++) {
                for (int i = 0; i < no_values; i++) {
                    filter[j][i] = Integer.parseInt(split_lines[j][i]);
                }
            }
        }
        catch (NumberFormatException e) {
            info_label.setText("ERROR: Invalid Character Found");
        }
        Object[] filter_info = new Object[2];
        int[] dimentions = new int[2];
        dimentions[0] = no_values;
        dimentions[1] = no_lines;
        filter_info[0] = filter;
        filter_info[1] = dimentions;

        return filter_info;
    }

This method is called FileFilterArray because it reads a filter into a file and stores it in an array. The method signature indicates that the method will eventually return an Object[] object. It also shows that the method takes the filter_file field as a parameter, this is a File.

The method body begins by defining the reader field, which is of type BufferedReader. It is initialised to null, which means it is empty. An ArrayList is then defined called lines. This will be used to store the lines of the file that will be selected by the user. We use "Generics" (introduced in Java 1.5) to indicate that this ArrayList will only be used to store String objects, which will provide type safety and allow us to avoid casting when we get the objects back out. Two variables to be used in loops are also defined, these are i and j and are of type int. Variables to be used in loops are know as loop variables.

A try block is used to attempt to read from the file that has been selected by the user. The value of reader is attempted to be updated by the result of a method call to BufferedReader, which takes the method call to FileReader as a parameter. These two methods are part of the io library class. The file that is passed to FileReader as a parameter is filter_file, which is the file that the user has selected. Essentially, this complicated code reads the file, which is then read by a buffered reader so it can be stored in the reader variable.

A for loop is then defined to read the value of reader into a variable of type String called line. This is achieved by calling the readLine method of the io library class, which will read the contents of reader line by line. While the value of line is not null, ie while the value of line is still being updated by the values of each line of reader, line is stored in a new location of the array lines.

If the code in the try block is unsuccessful, a number of exceptions could be thrown. The first exception is FileNotFoundException. This is dealt with in a catch block that changes the value of info_label to inform the user that the file cannot be found. This is done by calling the setText method of the awt library class. The second is an IOException. This is dealt with in the same way, but info_label is changed to inform the user that the file could not be read.

If no exceptions have been thrown, a finally block is executed. This contains an if statement that checks if the value of reader is not empty, if this is true, a try block is used to attempt to close the file. Always called

This can be achieved by calling the close method of the io library class. If this is problematic, an IOException is thrown, which is caught in a catch block, that alters the value of info_label to inform the user that the file cant be closed.

It is important to find out how many lines of values in the ArrayList lines. This is achieved by creating a variable no_lines of type int that stores the result of the method call size of the ArrayList library class on the lines ArrayList. It is also important to find out the number of values on each line of the lines ArrayList. This is achieved in a number of steps. The first step is to create a variable no_values, of type int. The second step is to get the first line of the lines ArrayList, which is achieved by calling the get method of the ArrayList library class, passing 0, which represents the first item as a parameter. The next step involves splitting the values into an array of strings using , as a delimiter. This is performed by calling the split method of the lang library class and passing ',' as a parameter. The fifth step is to return the length of the array of strings, separated by commas, by calling the length method of the ArrayList library class. This method call returns a value of type int, which can be stored in the variable no_values.

These values can now be stored in an array. The array is called filter and is a two dimensional array of type int. It is initialised with the values of no_lines and no_values. Another array needs to be initialised so we can split the values that are in the lines. This array is also a two dimensional array, but it is of type String. It is called split_lines, because thats exactly what it does.

The process described above for splitting up the values in the first line of the lines ArrayList needs to be repeated for each line. To do this, a for loop is defined. This initialises the loop variable i, which was defined at the start of the method, to 0. While i is less than the value of no_lines the loop body is executed. Each time the loop body is executed, i is increased by 1.

The loop body involves a similar process for splitting the values as previously described: the get method of the ArrayList is used to get the line that corresponds to the current value of i. This is split by calling the split method of the lang library class and passing ',' as a parameter. The result of this replaces the value of the first dimension of the array, previously no_lines with the new, split up data of the filter. The position it replaces it at is also the value of i.

The number of values in the new value of the first element of split_lines(at position i) needs to be compared against the number of values in theno_value field. A try block is used to attempt this. Inside the try block is an ifstatement. If the result of the method call length of the library class ArrayList returns a value that is not equal to the value of no_values an exception is thrown. Note that this is in the loop body, so this check is performed at every line in the filter. If an exception is thrown, it is caught in a catch block. This then calls the setText method of the awt library class to change the value of the info_label to inform the user that the filter that has been selected is not valid, as each row must have the same number of values.

The values are now prepared so they can be read into the filter array. This is achieved by setting up a for loop that iterates through the split_lines ArrayList by comparing the values of i and j to the values of no_lines and no_values, respectively. While the values of i and j are less than no_lines and no_values, the value of split_lines, at the position of i and j are converted into integers with the code Integer.parseInt, and then stored into the corresponding location of filter array.

The return type of this method is Object[], therefore, the values of filter, which is an array, need to be put into an Object[]. To do this, an Object[] of size 2 is created called filter_info, followed by a one dimensional array called dimentions. This has two values, which are no_values and no_lines. We place filter and dimentions as the elements in filter_info.

filter_info is then returned, to be used in the image processor application.

Creating Custom Filters

Ideally, users should be able to apply any filter they desire to an image. The method below shows the process of allowing users to creating their own filter.

<<Creating Custom Filter>>=
public int[][] CustomFilterArray(int height, int width) {
        int[][] filter = new int[height][width];
        DefaultTableModel table_model = 
(DefaultTableModel)filter_table.getModel();
        try {
            for (int j = 0; j < height; j++) {
                for (int i = 0; i < width; i++) {
                    filter[j][i] = 
Integer.parseInt((String)table_model.getValueAt(j, i));
                }
            }
        }
        catch (NumberFormatException e) {
            info_label.setText("ERROR: Filter Values Must Be Integers");
        }
        return filter;
    }

This method stores the values a user has entered for a custom filter into an array, therefore it is called CustomFilterArray. The method signature shows that it will return a two dimensional array of type int, which is indicated by the code int [][]. The method also takes height and width as parameters. These are type int.

The body begins by creating an array called filter and passing the values of height and width into its two dimensions. This array will be used to hold the filter.

A table model needs to be created. This can be achieved by calling the getModel method of swing library class and passing filter_table as a parameter. This is stored in the table_model variable, which is a DefaultTableModel type variable, so it needs to be cast as a DefaultTableModel.

A loop is going to be used to read in the text from a text field, so loop variables i and j are defined. The for loops are defined inside a catch block, as it may throw an exception. The first loop initialises j to 0, the second initialising i to 0. While j is less than height and i is less than width, the loop body is executed. Each time the loop body is executed, i and j are incremented.

The text from each text field is then attempted to be read using a try block. The getValueAt method of the TableModel library class is used to get the value. This value is in the form of a String. This value needs to be stored into filter, which is of type int, so the value needs to be converted using the code Integer.parseInt.If an exception occurs, it is caught in the catch block, which changes the value of info_label to inform the users that the filter values must be of type integer.

If no exceptions are called, the new values of filter are returned to be used in image processing using the return code.

Convolution

Convolution is an image processing technique where filters are applied to an image to achieve desired effects. These filters could be created by the user, or hardcoded filters that are in the program. Example effects include sharpen edges, blur the image to remove artifacts or pick out edges in an image and disregard the rest. The method that applies such techniques is written below.

<<Convolution>>=
public BufferedImage Convolve(BufferedImage image, int[][] filter, int x,int 
y) {
        int w=image.getWidth(), h=image.getHeight();
        byte[] data = GetImageData(image);
        int[][][] int_image = new int[h][w][3];
        int[][][] new_int_image = new int[h][w][3];
        int xCent = (x-1)/2;
        int yCent = (y-1)/2;
        int xMin = xCent*(-1);
        int yMin = yCent*(-1);
        for (int j=0; j<h; j++) {
            for (int i=0; i<w; i++) {
                for (int c=0; c<3; c++) {
                    int_image[j][i][c]=data[c+3*i+3*j*w]&0xFF;
                }
            }
        }
        for (int j=yCent; j<h-y+1+yCent; j++) {
            for (int i=xCent; i<w-x+1+xCent; i++) {
                int blue_total = 0;
                int green_total = 0;
                int red_total = 0;
                for (int yFilt=yMin; yFilt<y+yMin; yFilt++) {
                    for (int xFilt=xMin; xFilt<x+xMin; xFilt++) {
                        blue_total += 
filter[yCent+yFilt][xCent+xFilt]*int_image[j+yFilt][i+xFilt][0];
                        green_total += 
filter[yCent+yFilt][xCent+xFilt]*int_image[j+yFilt][i+xFilt][1];
                        red_total += 
filter[yCent+yFilt][xCent+xFilt]*int_image[j+yFilt][i+xFilt][2];
                    }
               }
               new_int_image[j][i][0]=blue_total;
               new_int_image[j][i][1]=green_total;
               new_int_image[j][i][2]=red_total;
            }
        }

        for (int j=yCent-1; j>=0; j--) {
            for (int i=xCent; i<w-x+xCent; i++) {
                new_int_image[j][i][0]=new_int_image[j+1][i][0];
                new_int_image[j][i][1]=new_int_image[j+1][i][1];
                new_int_image[j][i][2]=new_int_image[j+1][i][2];
            }
        }

        for (int j=h-y+yCent; j<h; j++) {
            for (int i=xCent; i<w-x+xCent; i++) {
                new_int_image[j][i][0]=new_int_image[j-1][i][0];
                new_int_image[j][i][1]=new_int_image[j-1][i][1];
                new_int_image[j][i][2]=new_int_image[j-1][i][2];
            }
        }

        for (int i=xCent-1; i>=0; i--) {
            for (int j=0; j<h; j++) {
                new_int_image[j][i][0]=new_int_image[j][i+1][0];
                new_int_image[j][i][1]=new_int_image[j][i+1][1];
                new_int_image[j][i][2]=new_int_image[j][i+1][2];
            }
        }

        for (int i=w-x+xCent; i<w; i++) {
            for (int j=0; j<h; j++) {
                new_int_image[j][i][0]=new_int_image[j][i-1][0];
                new_int_image[j][i][1]=new_int_image[j][i-1][1];
                new_int_image[j][i][2]=new_int_image[j][i-1][2];
            }
        }

        int cMin = new_int_image[0][0][0];
        int cMax = new_int_image[0][0][0];

        for (int j=0; j<h; j++) {
            for (int i=0; i<w; i++) {
               for (int c=0; c<3; c++) {
                   if (new_int_image[j][i][c] < cMin) {
                        cMin = new_int_image[j][i][c];
                    }
                    else if (new_int_image[j][i][c] > cMax) {
                        cMax = new_int_image[j][i][c];
                    }
                }
            }
        }

        for (int j=0; j<h; j++) {
            for (int i=0; i<w; i++) {
                for (int c=0; c<3; c++) {
                    data[c+3*i+3*j*w] = 
(byte)((new_int_image[j][i][c]-cMin)*255/(cMax-cMin));
                }
            }
        }
        return image;
    }

This method is called Convolve, which is a sensible name because it performs convolution on the image. The signature indicates that the method will return an object of type BufferedImage. It takes four parameters, the image that the methods alter, the array filter, x and y, which are both int.

The method body begins in very much the same way as the previous methods, getting image dimensions with getWidth and getHeight and declaring loop variables i, j, c, xCent, yCent, xFilt, yFilt, xMin, yMin, blue_total, green_total, red_total, cMin and cMax. A pointer to the data is also obtained in the same manner as previous methods.

Two 3-dimensional arrays are then defined, int int_image[][][] and new_int_image[][][], which are both of type int. int_image is used to store the current integer pixel values of the image and new_int_image will be used to store the new pixel values of the image as convolution is applied to it. Without the array for the new values the algorithm will be applied to a pixel then the new value for that pixel will be used when applying it to another pixel. The two arrays are initialised to the values of h, w and 3. These values represent height, width and the three colour channels.

To apply the convolution filter correctly, it needs to be placed on the image so the edges do not extend over the edge of the image. To achieve this, the centre point of the filter needs to be calculated, which is done by subtracting 1 from the value of x and dividing it by 2. The same is repeated for y and the result is stored in the fields xCent and yCent, respectively.

To find a starting point for where the filter is place, the distance from the centre needs to be discovered. This is achieved by multiplying the value of xCent by -1 and yCent by -1 and storing the result in xMin and yMin, respectively.

The loop body maps positions of pixel values of the rows columns and colour channels of a 3-d array to a 1-d array, but in the format that can be used to represent the new values, so they can be used in to the rows columns and colour channels of a 3-d array. This is achieved by multiplying c by 3, i by 3 and j by w and storing the result in the corresponding location of the data array.

Once the filter location is organised, the processing can be performed on the image. To begin the process, the rows and columns of the image are iterated through using for loops. The loop variable j is initialised to the value of yCent, while this is less than the value of the image height minus the value of the total of y plus 1 plus yCent, the loop body is executed. Each time the loop body is executed, the value of j is increased by 1. This is repeated for the columns by initialising i to the value of xCent and executing the body loop while i is less than the value of the image width minus the value of the total of x plus 1 plus xCent. Each time the loop body is executed, the value of i is increased by 1. As well as iterating through the rows and columns, these loops set centre points for the filter.

The loop body begins by setting the variables blue_total, green_total and red_total, which are the colour values to 0. A for loop is then used to iterate through the rows of the filter. yFilt is set the the value of yMin, while this value us less than the total of y plus yMin, the loop body is executed. Each time the loop body is executed, yFilt is incremented. A similar for loop is used to iterate through the columns of the filter, xFilt is set to the value of xMin and the loop body is executed while xFilt is less than the total of x plus xMin. Each time the loop body is executed, the value of xFilt is increased by 1.

The body of the loops that iterate through the filter values consists of multiplying the filter values with the corresponding image colour values. This is done for each colour channel, by firstly adding the value of the centre of the filter in the y direction (yCent) to the value of the distance from the centre in the y direction (yFilt). The value of the centre of the filter in the x direction (xCent) is then added to the distance from the centre in the x direction (xFilt). The total of the pixel of the image the filter is centered on in the y direction (j) and the distance from the centre in the y direction (yFilt) is then calculated. This is repeated for the x direction, using i and xFilt. These values are then multiplied together and are stored in their corresponding colour channel variable, for example, blue_total.

Once this complicated calculation is performed on each pixel, the new colour values for blue_total, green_total and red_total are stored in the corresponding locations in the new_int_image array.

Because the filter cannot be applied to the image in areas where certain areas may cover areas that aren't part of the image, certain areas of the image are not covered in the convolution process, namely the edges of the image. There are a number of techniques to deal with this problem, but this program deals with it by copying the values from a neighbouring pixel.

The first edge to be dealt with is the top edge. To solve the problem, a for loop is used to iterate through the rows and columns of the image. To iterate through the rows, j is set to the value of yCent minus 1. While j is greater than or equal to 0, the loop body is executed. Each time the loop body is executed, the value of j is decreased. To iterate through the columns of the image, i is set to the value of xCent. While i is less than the value of the image width minus the total of x plus xCent, the loop body is executed. Each time the loop body is executed, the value of i is increased.

The loop body is made up of code that copies the colour value of the pixel below the current pixel and stores it in the current pixel. This is done by storing the value of new_int_image[j+1] into the value of new_int_image[j] for each colour channel.

The same is repeated for the bottom edge, but j is increased each time the loop body is executed and the the loop body copies the colour value of the pixel above the current pixel and stores it in the current pixel. This is done by storing the value of new_int_image[j-1] into the value of new_int_image[j] for each colour channel.

To deal with the left edge, the same is done but the value of i is decreased each time the loop body is executed. The loop body copies the colour value of the pixel to the right of the current pixel and stores it in the current pixel. Storing the value of new_int_image[i+1] into the value of new_int_image[i] for each colour channel achieves this.

The right edge is dealt with the same manor, but the value of i is increased each time the loop body is executed. The loop body is made up of code that stores the value of new_int_image[i-1] into the value of new_int_image[i] for each colour channel.

The maximum and minimum colour values of the new image need to be found so normalisation of the colour values can occur. To start, the values of cMin and cMax are set to the first colour values in new_int_image. The rows, columns and colour channels are then iterated so each value of each colour channel is compared to the current value of cMin and cMax. This is performed by setting up three for loops, initialising each loop variable i,j and c to 0. The loop body is executed while j is less than the image width, i is less than the image height and c is less than 3, which is the number of colour channels. Each time the loop body is executed, each loop variable is incremented.

The loop body begins with an if statement. This is used to check if the current locations colour value, ie new_int_image[j][i][c] is less than the value of cMin. If it is, the value of cMin is replaced with the colour value at location new_int_image[j][i][c].

If the value is greater than the value of cMin, an else if statement is used to detect if the current pixels colour value is greater than the value of cMax. If it is, the value of cMax is replaced with the colour value at location new_int_image[j][i][c].

Now that the maximum and minimum colour values have been stored in the variables cMax and cMin respectively, the process of normalisation of the colour values can occur. The same three for loops as above are used to iterate through the rows, columns and colours.

The loop body consists of one statement that stores the result of a complicated calculation that multiplies the current pixel value minus the value of cMin by 255, which represents the number of possible colour values in the rgb colour scheme. This value is then divided by the result of subtracting cMin from cMax. This value is then cast into a type byte because it is going to be stored in its correct location in the 1-dimensional array data, which is of type byte.

A return statement is then used to return the image variable so it can be displayed in the GUI.

Blue Fade

The final stage of processing on the image is called blue fade. The method below shows how this is achieved.

<<Blue fade>>=
public BufferedImage BlueFade(BufferedImage image) {
        // Get image dimensions, and declare loop variables
        int w=image.getWidth(), h=image.getHeight();
        // Obtain pointer to data for fast processing
        byte[] data = GetImageData(image);

        int[][][] int_image = new int[h][w][3];

        // Copy byte data to new image taking care to treat bytes as unsigned
        for (int j=0; j<h; j++) {
            for (int i=0; i<w; i++) {
                for (int c=0; c<3; c++) {
                    int_image[j][i][c]=data[c+3*i+3*j*w]&0xFF;
                } // Colour loop
            } // Column loop
        } // Row loop

        // Now carry out processing on this different data typed image (e.g. convolution or "bluefade")
        for (int j=0; j<h; j++) {
            for (int i=0; i<w; i++) {
                int_image[j][i][0]=255*j/h; // BLUE
                int_image[j][i][1]=0; // GREEN
                int_image[j][i][2]=0; // RED
            } // Column loop
        } // Row loop

        // Now copy the processed image back to the original
        for (int j=0; j<h; j++) {
            for (int i=0; i<w; i++) {
                for (int c=0; c<3; c++) {
                    data[c+3*i+3*j*w]=(byte) int_image[j][i][c];
                } // Colour loop
            } // Column loop
        } // Row loop

        return image;
    }

The method name BlueFade shows what processing this method will perform on the image. The signature also shows that it will take image as a parameter and eventually return it.

The method body begins in much the same way as the other methods in the ImageProcessor class, getting the image dimensions, obtaining a pointer to the image data an initialising a 3-dimensional array int_image which will be used to represent the image as an array. There is also a variable t which is of type double.

The positions of pixel values of the rows columns and colour channels of a 3-d array to a 1-d array need to be mapped, but in the format that can be used to represent the new values, so they can be used in to the rows columns and colour channels of a 3-d array. This is achieved by multiplying c by 3, i by 3 and j by w and storing the result in the corresponding location of the data array.

Once the byte data has been copied into the int_image the image processing can occur. Two for loops are created to iterate through the columns and the rows of the image, firstly j and i are set to 0. While j is less than the image height and i is less than the image width, the loop body is executed. Each time loop body is executed, i and j are executed.

The loop body is made up of three statements, the first statement changes the blue colour value at pixel position int_image[j][i][0] to the result of the calculation of 255 multiplied by the value of j, divided by the value of h. The two other statements set the colour values of green and red to 0.

Main Method

There needs to be a main method in the ImageProcessor class so it can be run. The code for the main method is written below.

<<Main>>=
public static void main(String[] args) throws IOException {
        ImageProcessor e = new ImageProcessor();
        e.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        e.ImageProcessor();
    }

The main method should only be executed once, therefore it is static. It does not return anything to the user, so it is of type void. The method signature also allows parameters to be passed to it through the command line. This is achieved by creating an array of type String called args. The main method should always be able to execute by default, so it throws an IOException, which can be dealt with if there are problems in execution.

The method body is made up of three lines, the first line creates a new instance of the ImageProcessor class, called e.

The second line defines the operation that should be performed on the class when it is closed by calling the setDefaultCloseOperation of the swing library class and passing the EXIT_ON_CLOSE method of the swing library class as a parameter. This tells the JFrame component of the GUI to close on exit.

Once this is set, the ImageProcessor class is called, which allows the program described above to be run.

<<ImageProcessor.java>>=
Defining the class
Download code
Personal tools