Week 4 Lab: Image Manipulator 3000, Part II

Objectives

Overview

In this assignment you will build on the solution created for Lab 3. In addition to updating your solution based on feedback from your instructor, you will add support for a new custom binary image format, additional image transformations, and additional user interface elements to support the new functionality.

Assignment

You will implement the following functionality:

All of your classes should be placed in a package whose name matches your MSOE username.

Details

Handler Methods

You will need to add these methods to your Controller class:

Refactoring for Functional Programming

You must refactor your implementations of the grayscale() and negative() so that they make use of a functional programming approach. This same approach must be used in the implementations of red() and redGray().

You must implement the following method in your Controller class:

private Image transformImage(Image image, Transformable transform) {
    // ...
}

This method applies the specified transformation to each pixel in the image to produce a transformed image. The second argument specifies the behavior of the desired transformation.

The Transformable interface is a functional interface. A functional interface has a single method, and can therefore be implemented with a lambda expression. The method, named apply(), must accept two arguments: the y location of the pixel and its color. The method must return the color for the pixel after the applying the transformation. You must specify implementations of the Transformable interface for the grayscale, red, redGray, and negative image transformations as lambda expressions.

Red Only

This transformation acts as a red filter. The green and blue components of each pixel are set to zero.

Red Only

Red Only Example

Red-Gray

This transformation is based on an experiment demonstrated by Edwin Land, founder of Polaroid, in 1959 more info. The experiment was based on the Retinex Theory of Color Vision. This page has a nice explanation of the experiment and the Retinex theory. The transform is quite simple: perform a Grayscale transformation on the pixels in alternating rows and perform a Red Only transformation on the pixels in the other rows2).

Red-Gray

Red-Gray Example

Filter Kernel Window

When clicked, the Show Filter button reveals a second window and changes the text on the button to Hide Filter. When Hide Filter is clicked, the second window is hidden, and the text on the button changes back to Show Filter.

User Interface

Filter Kernel Window

This window is specified in the filter.fxml file. The start() method will now need load both FXML files and create a second Stage to place the scene described in this FXML. The filter.fxml file must be associated with the FilterController class that contains a two dimensional array of TextFields, kernel that references the 3 x 3 values for the filter kernel.

Once the FXMLLoader has loaded the filter.fxml file and created the appropriate UI objects, you'll need to assign the elements of the kernel array to reference the FXML TextFields. To do this, your FilterController will need to implement the Initializable interface. In the initialize() method, you can assign the elements in kernel.

In addition, your FilterController must have the following event handlers:

The Filter Kernel window contains a 3 x 3 grid3) of filter weights. The user must be able to modify these weights manually or by clicking the Blur or Sharpen buttons. When clicked, the Blur or Sharpen button on the right change the filter weights as follows:

Blur          Sharpen
0  1  0        0  -1   0
1  5  1       -1   5  -1
0  1  0        0  -1   0

The Apply button applies whatever values are in the filter kernel to the image. At a minimum, the button need only apply the filter kernel if the sum of the filter kernel weights is a positive, non-zero value. For example, if the user enters 0 for all the filter weights, applying the filter can produce unpredictable results. The weights in the array specifying the filter kernel should sum to 1. Since the blur filter kernel has values that sum to 9 (0+1+0+1+5+1+0+1+0), each value must be divided by 9. The following code applies the Blur filter kernel:

double[] kernel = { 0.0,  1.0/9,  0.0,
                   1.0/9, 5.0/9, 1.0/9,
                    0.0,  1.0/9,  0.0};

The values for filter kernel for Sharpen sum to 1 (5-1-1-1-1) so the weights in the array here would be:

double[] kernel = { 0.0, -1.0,  0.0,
                   -1.0,  5.0, -1.0,
                    0.0, -1.0,  0.0};

Blur

Blurred (applied 10 times) Example

Sharpen

Sharpened (applied 1 time) Example

The Apply button must be disabled whenever an unsupported set of weights are entered in the window.

ImageFilter Utility Class

All image filtering and convolution logic must be implemented in a separate utility class named ImageFilter.

This class is responsible for:

The controller must not perform kernel math or convolution directly. Instead, it must delegate filtering operations to ImageFilter, which will have at a minimum one public method, applyKernel(Image image, int[][] kernel) that will validate the input parameters so that:

Once the parameters are validated, applyKernel() will "normalize" the kernel so the weights sum to 1.0 (by dividing each value by the sum of the values). It will then call ImageUtil.convolve to transform the image:

Image result = ImageUtil.convolve(originalImage, normalizedKernel);

Loading and Saving Images

Your program must now support loading and saving .bmsoe files. Add the following two methods to your ImageIO class and modify your read() and write() methods to make use of these new methods when appropriate:

.bmsoe Binary Image Format

The custom .bmsoe file format is a binary based file format for storing images. It is designed to be easy to load and save as well as produce smaller file sizes than the .msoe format.

The file consists of a stream of binary data with the following header information:

The remainder of the file contains the pixel data. Each pixel is stored as a single integer value with bits 24-31 representing the alpha channel, bits 16-23 representing the red channel, bits 8-15 representing the green channel, and 0-7 representing the blue channel.

This additional code have been added to your ImageIO class to facilitate conversion between Color and int:

    private static final int BIT_MASK = 0x000000FF;
    private static final double BYTE_LENGTH = 255.0;
    private static final int ALPHA_SHIFT = 24;
    private static final int RED_SHIFT = 16;
    private static final int GREEN_SHIFT = 8;
    
    private static Color intToColor(int color) {
        double red = ((color >> RED_SHIFT) & BIT_MASK)/BYTE_LENGTH;
        double green = ((color >> GREEN_SHIFT) & BIT_MASK)/BYTE_LENGTH;
        double blue = (color & BIT_MASK)/BYTE_LENGTH;
        double alpha = ((color >> ALPHA_SHIFT) & BIT_MASK)/BYTE_LENGTH;
        return new Color(red, green, blue, alpha);
    }

    private static int colorToInt(Color color) {
        int red = ((int) Math.round(color.getRed() * BYTE_LENGTH)) & BIT_MASK;
        int green = ((int) Math.round(color.getGreen() * BYTE_LENGTH)) & BIT_MASK;
        int blue = ((int) Math.round(color.getBlue() * BYTE_LENGTH)) & BIT_MASK;
        int alpha = ((int) Math.round(color.getOpacity() * BYTE_LENGTH)) & BIT_MASK;
        return (alpha << ALPHA_SHIFT) + (red << RED_SHIFT) + (green << GREEN_SHIFT) + blue;
    }

Your program should be able to load the specs.bmsoe image file in the images folder of the repository.

Alpha Channel Complexities

You may find that some images appear all black when saved in the .bmsoe format. This occurs when the original image does not have alpha channel information. In that case, the saved images will have values of 0 for all of the alpha channel values. You do not need to support saving such images.

Exception Handling

There are a number of situations that could cause your program to throw an exception. For example, if the file is not found, cannot be opened, or contains incorrectly formatted data, it is likely that an exception will be thrown. In these cases, the program should display a useful message and recover gracefully.

(Optional) Keyboard Shortcuts (Accelerators)

If you are using Accelerators in your program, use the following key mappings for the Accelerators:

Just For Fun

There are many additional enhancements that could build on the required functionality. You are encouraged to enhance this application using your creativity. A number of enhancements are included below; however, you should not feel limited to these suggestions.

Acknowledgment

This laboratory assignment was developed by Dr. Chris Taylor.

1) These methods will likely be part of a separate controller class associated with the second stage.
2) This is a slight simplification from the actual transformation. You may choose to implement the actual transformation, if you choose to (you'll need to look it up). Be sure to indicate with a comment if you choose to do this.
3) You may choose to make a 5 x 5, 7 x 7, or 9 x 9 grid instead.
See your professor's instructions for details on submission guidelines and due dates.