Week 4 Lab: Image Manipulator 3000, Part II
Objectives
- Employ binary file input and output to read and write image files.
- Construct a non-trivial interactive graphical user interface with at least two windows.
- Handle exceptions in a robust and meaningful fashion.
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:
- Load and save images in the custom binary
.bmsoefile format. - Convert an image to a red-channel version
- Convert an image to a red/grayscale version
- Refactor the code for grayscale and photo negative transformations using a functional programming approach
- Create a second window (an additional stage) for entering filter parameters.
- The appropriate graphical user interface to support the operations listed above.
- Correct any issues identified in your previous submission, if your instructor has provided feedback to you in a timely manner.
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:
void red(ActionEvent event)— Handler for converting an image to red-channel only.void redGray(ActionEvent event)— Handler for converting an image so that every pixel on an even numbered line contains only the red color data and every pixel on an odd numbered line contains the grayscale representation of the color.void showFilter(ActionEvent event)— Handler to toggle the visibility of the Filter Kernel window. When the window is not visible, the button should display "Show Filter." When the window is visible, the button should display "Hide Filter."
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-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).
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.
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:
void updateFilterValues(ActionEvent event)— Handler for updating filter kernel values based on the preset selected.1)void apply(ActionEvent event)— Handler for applying a filter operation to an image.1)
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};
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:
- Validating filter kernels
- Normalizing kernel values
- Applying convolution using
ImageUtil.convolve
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:
- The image is not null
- The kernel weights do not sum to zero
- The kernel is a 3x3 grid
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:
readBMSOE(Path path)— Reads an image file in.bmsoeformat.writeBMSOE(Image image, Path path)— Writes an image file in.bmsoeformat.
.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 characters
B,M,S,O,Eeach written as abyte. - The image width written as an
int - The image height written as an
int
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:
- Ctrl-Shift-R: Red
- Ctrl-Shift-G: Red-Gray
- Ctrl-F: Toggle Filter Window
- Note that you would need to attach this Accelerator to both the main window and the filter window
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.
- Add a menu to the main window to replace (or in addition to) the buttons
- Apply a transform to only a selected region of the image
- Support saving
.bmsoeimages when the input image does not have alpha channel values. - Display the original and transformed images side-by-side
- Toggle between original and transformed images when mouse button is pressed on the image
- Create a meme generator by adding styled text on the image
- Tonal adjustment (e.g., adding a red hue to the image underneath the mouse)
- Provide undo functionality
- Implement additional transformations, e.g.
- Brighten
- Darken
- Decrease color saturation
- Increase color saturation
- Apply additional filters like the Edge filter see here
Acknowledgment
This laboratory assignment was developed by Dr. Chris Taylor.
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.




