Project 1: Picture of Incompetence
Due by: Friday, September 20, 2024 at 11:59 p.m.
"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). |
|
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. |
|
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. |
|
v | Vertically mirror the bitmap. Flip the pixels of the bitmap around the x-axis. |
|
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. |
|
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. |
|
r | Rotate the image 90 degrees to the right (clockwise). |
|
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:
- 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. - 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. - 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. - 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.*
andjava.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 calledBitmap.java
. You must also have a main program that reads the commands and reacts appropriately. This class should be calledManipulator
and stored in a file calledManipulator.java
. You may write other classes if you find them useful. One useful idea is aPixel
orColor
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 likePixel
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.