Project 1: Picture of Incompetence

Due by: Friday, September 20, 2024 at 11:59 p.m.

Jonah: You're a meme, ma'am.

-Veep

"Uh oh..."

Jonah Ryan has just spilled a jug of Lysol mixed with vodka all over the latest publicity photos of Selina Meyer and her team! Some dangerous chemicals must have been in the photos because they're doing all kinds of strange things: stretching, shrinking, turning bizarre colors. As the only computer scientist working for the Vice President, you must put it all right.

The Mission

In order to reconstruct the images, we need you to write a program that can read in a bitmap file and perform a sequence of modifications consisting of the following seven operations, depending on the commands given by the user.


User Command Operation Example
i Invert the colors of the bitmap. If the original color of a pixel was (R, G, B), the new color value should be (255 - R, 255 - G, 255 - B). Invert Example
g Make the image grayscale. Set the R, G, and B values for a single pixel to one value. That value should be: .3R + .59G + .11B, rounded to the nearest integer.

Note that when the red, green and blue components are all the same, the color is a shade of gray. For example (0,0,0) is black, (255, 255, 255) is white, and (128, 128, 128) is a 50% gray.
Grayscale Example
b Blur the image. For each pixel, make each of its color components the weighted average of all the pixels in a two pixel radius, rounding to the nearest integer.

For a normal pixel, doing so means adding up the color values for a 5 × 5 square of 25 pixels. (Two rows above and below, two columns to the left and to the right.) The red, green, and blue sums should each be divided by 25.

Of course, any pixels which are close to an edge should only average pixels that are part of the image. Thus, a corner pixel will generally be the average of a 3 × 3 square of pixels. (Two rows down and two columns to the right only.) Clever use of loops and counting the number of pixels summed can take care of these edge cases without too much additional code.
Blur Example
v Vertically mirror the bitmap. Flip the pixels of the bitmap around the x-axis. Vertical Mirror Example
s Shrink the bitmap to half its size in both height and width directions. Because the bitmap will cover 1/4 the area that it did before, you will have to average the color values for four pixels into color values for one pixel. Averaging should be done by converting the values to floating point values, averaging them together, and rounding to the nearest integer.

If either the height or width of the image are not even, ignore the last row or column that makes the image odd.
Shrink Example
d Double the size of the bitmap in both height and width directions. You will have to make four pixels have the same color values as one pixel did in the original. Double Example
r Rotate the image 90 degrees to the right (clockwise). Rotate Example


When your program starts, it should prompt the user for a file to open. If the file is invalid, it should quit gracefully and print an appropriate error message. Otherwise, it should run a loop, asking the user to input commands to alter the image. Legal commands are: i, g, b, v, s, d, r, and q to quit. When the q option is given, the program should prompt the user for a file to save the altered image into.

Some sample execution output is given below. User input is given in green.

What image file would you like to edit: image.bmp
What command would you like to perform (i, g, b, v, s, d, r, or q): s
What command would you like to perform (i, g, b, v, s, d, r, or q): i
What command would you like to perform (i, g, b, v, s, d, r, or q): r
What command would you like to perform (i, g, b, v, s, d, r, or q): q
What do you want to name your new image file: edited.bmp

This sample execution opens a file called image.bmp, shrinks it, inverts its colors, rotates it 90° to the right, then saves it to edited.bmp.


Binary Finary

The first task you need to attack is reading in the bitmap. You will need to allocate enough space to hold the data in the bitmap. How can you do that? Well, at the beginning of a bitmap file is a header which gives a lot of information about the file. If we were coding in C or C++, we could dump that data directly into a struct. Unfortunately, Java doesn't like giving us direct access to memory. So, we'll have to read everything in piece by piece. The header is laid out as follows.

byte 	type0;			// always contains 'B'
byte 	type1;			// always contains 'M'
int	size;			// total size of file
int	reserved;		// always 0
int	offset;			// start of data from front of file, should be 54
int	header;			// size of header, always 40
int	width;			// width of image in pixels
int	height;			// height of image in pixels
short	planes;			// planes in image, always 1
short	bits;			// color bit depths, always 24
int	compression;		// always 0		
int	dataSize;		// size of color data in bytes
int	horizontalResolution;	// unreliable, use 72 when writing
int	verticalResolution;	// unreliable, use 72 when writing
int	palette;		// colors in palette, use 0 when writing
int	importantColors;	// important colors, use 0 when writing

If you are interested, there is a good explanation of the file format here.

Note that a byte takes up 1 byte of space. An short takes up 2 bytes of space. An int takes up 4 bytes of space.

But how do you read them? Java provides a number of ways to read binary data directly from a file, but I recommend that you use the BufferedInputStream class. BufferedInputStream doesn't have a lot of frills. The only methods you'll need are the skip(), read(), and close() methods. The skip() method will skip however many bytes you specify. The read() method will either read a single byte or an array of bytes. The close() method closes the file. To make a BufferedInputStream, first create a FileInputStream and use it in the constructor for the BufferedInputStream. You could use a FileInputStream directly, but the BufferedInputStream will make the performance much better.

One Little Endian

In the bitmap file format (and on Intel machines in general), values are stored in Little Endian byte ordering. This means that the bytes of an int value are stored least significant byte first. When you go to reconstruct an int from four bytes that you've read in, you should use the bitwise left shift operator (<<) to shift each of the four bytes by the appropriate amount and add them together.

The process is similar for a short, except that there are only two bytes to worry about.

For more information, read the Wikipedia article on Endianness.

Writing the Files

When writing the files, create an object of type BufferedOutputStream. The only methods you'll need to worry about are write() and close(). One version of the write() method writes a single byte and one writes an array of bytes. Similar to the BufferedInputStream, first create a FileOutputStream and use it in the constructor for the BufferedOutputStream.

When you write the header of the file, you need to output all the bytes for all the values, using the default values given above for values that you neither store nor can compute. You will need to decompose int and short values into their component bytes, in the correct order, and output them. You should use the unsigned right shift operator (>>>) to do so.

Color Data

The offset member of the header says where the color data starts in the bitmap. This value should be 54, but if it's larger, you should skip bytes after the header until you make up the difference. We will only deal with 24-bit bitmaps. That means that each pixel is represented by three bytes. The first byte is the blue value, the second byte is the green value, and the third byte is the red value. The first pixel that you read in will be the pixel in the lower left-hand corner of the bitmap. As you read in pixels, you will move across the row until you finish the row, then move onto the row above. Although bitmaps are read from the bottom up and with the color values in BGR order (instead of the more standard RGB), neither of these facts should affect your code much.

You should allocate a 2D array to hold the color data. While it's possible to use a 2D array of byte values, a better approach would create some kind of Pixel or Color object (whose class you will have to define). For each such object, you'll need to read three bytes: one for each of the blue, green, and red color components.

Unfortunately, it's not quite that simple. The bitmap standard requires that the number of bytes in a row of pixels falls evenly on word boundaries, in other words, is evenly divisible by 4. If the number of bytes used to represent a row of pixels is not evenly divisible by 4, it will be padded with zeroes until it is. You must read these zeroes in and discard them when getting the bits. Likewise, you have to remember to appropriately repack with zeroes when writing the file back out.

For padding, there are four different possibilities:

  1. The bytes in a row is evenly divisible by 4. No padding is needed.
    Example: Width = 100. Given 3 bytes in a pixel, 100 × 3 = 300 bytes in a row. There is no padding.

  2. The bytes in a row has a remainder of 3 when divided by 4. Padding of 1 byte occurs.
    Example: Width = 101. Given 3 bytes in a pixel, 101 × 3 = 303 bytes in a row. There needs to be 1 byte padding in the row to make 304 bytes.

  3. The bytes in a row has a remainder of 2 when divided by 4. Padding of 2 bytes occurs.
    Example: Width = 102. Given 3 bytes in a pixel, 102 × 3 = 306 bytes in a row. There needs to be 2 bytes padding in the row to make 308 bytes.

  4. The bytes in a row has a remainder of 1 when divided by 4. Padding of 3 bytes occurs.
    Example: Width = 103. Given 3 bytes in a pixel, 103 × 3 = 309 bytes in a row. There needs to be 3 bytes padding in the row to make 312 bytes.

Even more bizarrely, the strictest interpretation of the bitmap standard requires the total size of the file to be evenly divisible by 4. Since the header is 54 bytes (not divisible by 4) and the color data is always evenly divisible by 4, you should always output an extra two bytes of zeroes after outputting the color data. Thus, the dataSize that you output should be height × ((width × 3) + padding), and the overall image size that you output should be 54 + dataSize + 2.

Signed vs. Unsigned

The bitmap standard assumes that the bytes representing colors are unsigned, with the range 0 - 255. The Java byte type is signed and interprets these values to be in the range -128 - 127. If you're just swapping byte values around, as you will in the vertical mirroring or rotating operations, this fact is unimportant.

However, for the shrink, grayscale, invert, blur, and median filter operations, you might have to do arithmetic on the byte values. I recommend that you store your red, green, and blue values as int values, but if you store them as byte values, you will need to convert them to int values to do your arithmetic. If a byte value is positive, the unsigned version is the same. If a byte value is negative, add 256 to it to get the equivalent. Oddly enough, if you store an unsigned value into a signed byte, its value is stored correctly. So, you only have to do the conversion one way.

If your images look mostly correct but have odd, brightly colored noise in some parts, you have probably messed up the conversion to unsigned.

Hints

  • Start early. This project is tough. Do everything you can to work smoothly with your partner(s).

  • Reading in (and writing out) the bitmap data is a huge part of the problem. Once the data is in, if you have stored it in a reasonable way, doing the actual transformations should be relatively straightforward. It's a good idea to let one member of the group work just on input and output and the other do the bitmap transformations. Also, you should test your code by reading in an image and then outputting it directly. If it looks exactly the same, you're on the right path.

  • Make sure you test rectangular (non-square) bitmap images, especially with rotation! Code that works for square images without padding has no guarantee that it will work for messier cases.

  • You should only read in data from a file once and write it out once. It does not make sense to output the bitmap data to a file after each operation.

  • You may need to visualize the bitmaps you are creating to see how you're doing. The Windows image viewers should be sufficient for this purpose. If the image won't load or doesn't look right, you have corrupted its header or data somehow. Also, you should try to limit the size of the bitmaps you use for testing since they take up a lot of space.

  • Work incrementally. Compile constantly. Get bitmaps inputting and outputting without any operations first. Then, add in simple operations like inverse. You may want to print out the header information for testing purposes (or at least view it in the debugger).

  • Write clean code. You can easily make code so complicated that no one (including your instructor) can figure out why it doesn't work.

  • Do not import any packages other than java.io.* and java.util.Scanner.

  • You are expected to create a class called Bitmap to hold bitmap data. Each transformation (such as shrink or double) should have a corresponding member function. Your class should be stored in a file called Bitmap.java. You must also have a main program that reads the commands and reacts appropriately. This class should be called Manipulator and stored in a file called Manipulator.java. You may write other classes if you find them useful. One useful idea is a Pixel or Color class that holds the red, green, and blue values for each pixel. Note that storing color data as a 2D array of bytes is a very bad idea. By loading the data into a 2D array of something more logical like Pixel objects, you can do manipulations at a higher level of abstraction, rather than worrying about the ordering of bytes. Byte ordering should be dealt with only when doing the actual file input and ouptut.

  • Plan out your code ahead of time. Good design saves on coding.

  • Refer to the coding standards page for more information on what I'm looking for in terms of style.

Provided Images Files

No Padding Required

Padding Required

Submit to the Will of Selina Meyer

Zip up all the .java files you use to solve this problem from your src directory, including Bitmap.java and Manipulator.java, and upload this zip file to Brightspace. Zip up only the source files, not the entire project. All work must be submitted by Friday, September 20, 2024 at 11:59 p.m. unless you are going to use a grace day.

I must be able to compile your program with the command javac Manipulator.java and run with the command java Manipulator.

All work must be done within assigned teams. You may discuss general concepts with your classmates, but it is never acceptable for you to look at another team's code. Please refer to the course policies if you have any questions about academic integrity. If you have trouble with the assignment, I am always available for assistance.

Grading

Your grade will be determined by the following categories:

Category Weight
Inverting Images 10%
Making Images Grayscale 10%
Blurring Images 10%
Vertically Mirroring Images 10%
Shrinking Images 15%
Doubling Images 15%
Rotating Images 10%
Correct Command Processing 10%
Style and Comments 10%

Note: Submissions which do not compile will automatically score zero points.

Under no circumstances should any member of one group look at the code written by another group. Tools will be used to detect code similarity automatically.