Grayscale Image


General

  • All submissions must include a work history to be worth credit.
  • Submit all files to elearn. Make sure to name file(s) EXACTLY as specified - including capitalization.
  • Make sure your program accepts input in EXACTLY the order and format given. Do not ask for extra input or change the order of inputs.
    If you want to make an expanded version of a program that and get feedback on it, email it to me or show me in class.
  • Slight variations in output wording/formatting are usually fine (as long as formatting isn't part of the assignment).
    If you do not get the right final output, print out some evidence of the work your program did to demonstrate your progress.
  • Readability and maintainability of your code counts. Poor formatting, confusing variable names, unnecessarily complex code, etc… will all result in deductions to your score.

Background

The most common way to store image data is as a "bitmap" - a 2D grid of numbers representing the color of each pixel or square in the image. In a grayscale image (what people would normally refer to as black and white) the value of each pixel is typically stored as a number between 0 for black and 255 for white.

A heart as grayscale a bitmap

Here is the same image (a heart) shrunk down:

A heart as grayscale a bitmap

Assignment Instructions

Submit file: assign5.zip

Make sure to compress your entire folder, not just your code file.

I should be able to add my own copies of GrayscaleImage.h and doctest.h and build your project with the following command:

g++ -g -std=c++17 GrayscaleImage.cpp tests.cpp -o tests.exe

Your task is to make a class that represents a grayscale image. A GrayscaleImage will track its width and height (which will be fixed at creation) and an array of pixels which will be stored as the data type uint8_t. A uint8_t is an "unsigned 8-bit integer" data type. It is like an int but it can only hold the values 0-255 (all the unsigned values you can represent with 8 bits).

The array of pixels you store will be a 1D array as there is no clean way to dynamically allocate a 2D array in C++. Fortunately, it is easy to map a 2D array with dimensions [ROWS][COLS] onto a 1D array with size [ROWS * COLS]. The image below shows how a 2x3 2D array corresponds to a 6-element 1D array:

6 value array compared to 2x3 2D array

Notice that the entire first row of data from the 2D structure comes first in the 1D array, then all the data from the second row of data, then (if it exists) the third row... Because of this, you can calculate the correct location [r][c] in the 2D structure as [r * COLS + c] in the 1D structure, where COLS is the number of columns. For example: the yellow highlighted cell is at [1][0] in the 2D array and there are 3 columns. So to find the same value in the 1D array we can calculate [1 * 3 + 0] => [3].

To manage this, you are provided helper functions uint8_t getPixel(int row, int col) const and void setPixel(int row, int col, uint8_t brightness) already in the starter code. Any time you want to get or set the value of a pixel, you should use these functions to access the correct location in the 1D array you are actually using.

You are provided a GrayscaleImage.h, a partial GrayscaleImage.cpp, and some sample tests: tests.cpp. You should not modify GrayscaleImage.h. You should only modify tests.cpp to comment out entire TEST_CASEs that do not compile with your code or that cause a crash. Do not modify a TEST_CASE in any way except to comment the entire thing out. Leave in any TEST_CASES that compile and run, even if they fail.

Passing Test > Failing Test > Commented-Out Test > Does not Compile

See below for tips on specific functions.

Getting Started

You should make a UnitTest project and add the provided files to it, replacing the original tests.cpp.

Update the Makefile so that the HEADERS and TEST_FILES list the .h and .cpp files respectively.

# list .h files here
HEADERS = GrayscaleImage.h

# list .cpp files here
TEST_FILES = tests.cpp GrayscaleImage.cpp

Comment out all the tests but the first one in tests.cpp. Implement the constructor and work until you pass test 1. Then, work on one new test at a time, implementing only the new functions needed to pass that test.

Memory Management

Make sure to test your code for leaks or other memory errors with Valgrind (or leaks on a Mac).

Test early and often. It will be much easier to find and fix memory issues write after you write the relevant code than if you wait until you have written all the functions and then test with Valgrind (or leaks on a Mac) for the first time. Valgrind should give your program a clean bill of health. Any errors reported by Valgrind will result in a significant deduction, even if the program appears to work fine despite of them.

In particular, watch out for memory leaks. Memory leaks will not cause any visible errors in your program—the only way to know that you have a leak is to use Valgrind (or run your code long enough that you can measure a steady increase in used memory).

Remember, Valgrind shows you where it detected an issue. Often, the place where you caused the issue is somewhere else! For example, maybe your constructor did not allocate an array but that was not a problem until later when you went to write to that array.

Function Tips

Here is a guide to function implementations. They are provided in a suggested implementation order.

Remember to use getPixel and setPixel anytime you want to get or set a pixel using its "2D coordinates".

Also, remember that uint8_t is the data type to use for pixel gray values. It works just like an int would only holds values from 0-255. You can freely copy a uint8_t into an int, but if you want to copy an int into a uint8_t without warnings, you need to do something like:

int x = 200;
uint8_t pixel = static_cast<uint8_t>(x);   //trust me, I know x is going to fit into pixel
  • GrayscaleImage::GrayscaleImage(int heightVal, int widthVal)

    Our basic constructor — it should create a GrayscaleImage with the given dimensions and allocate an array big enough to hold width * height number of uint8_ts. The array should be initialized to all 0's (black).

  • GrayscaleImage::fill(uint8_t brightness)

    Set every pixel of the image to the given brightness value.

  • string toString() const

    This function will mostly be useful for debugging purposes. While troubleshooting, you can use it to print out an image using something like:

    cout << image.toString();

    Read the .h for expected format. Your output must have a \t after each pixel and a \n at the end of each row. You will want to use to_string(num) to convert numbers to strings.

    Overriding the << operator is the more idiomatic C++ way of doing output: cout << image; instead of cout << image.toString(); If you want, feel free to experiment with it in a copy of the project. But since you can't modify the .h file, you can't substitute it for toString in the version you turn in.

  • bool operator==(const GrayscaleImage& other) const

    Checks to see if two images are exactly the same. (Same width, height, and pixel values).

    Warning: you can't just compare the pixels arrays themselves with ==; that will compile, but it will just compare the memory addresses of the two arrays. You need to loop through the pixels to compare the individual values.

  • ~GrayscaleImage()

    Destructor. Not tested directly—but every test will leak memory without this. Verify it works by running your program with Valgrind (or leaks on a Mac).

  • GrayscaleImage(const GrayscaleImage& other) and GrayscaleImage& operator=(const GrayscaleImage& other)

    Copy constructor and assignment operator. Tested in two unit tests. These are another big area for memory issues — test with Valgrind (or leaks on a Mac)!

    The tests for many other functions will rely on the copy constructor. You need to get it working before finishing most of the rest of the assignment.

  • GrayscaleImage addFrame(int padding, uint8_t brightness) const

    This should make a new GrayscaleImage that is a copy of this one but has a "frame" of extra pixels around it on each side. The original image will NOT be modified. padding describes how many pixels worth of frame should surround the contents of the original image and brightness the color value to give them.

    Here is what it might look like after applying (right side) a frame with 2 padding and a brightness of 32 to what originally was a 2x4 image (left side).

    Applying a 2 pixel frame with color value 32 to a 2x4 image

This function needs to make a GrayscaleImage that will be returned (the new "framed" version). However, that does not mean you need to use the new keyword in this function.

GrayscaleImages manage heap based memory - they do not belong on the heap themselves. Allocate the "new" object as a local variable (on the stack) and return it. When you make the object, it will make an array on the heap (in its constructor) and eventually release that memory (destructor).

You technically could do new GrayscaleImage() and then use the resulting pointer to keep track of the object, but that is needlessly complex - the caller would need to make sure to manually delete the GrayscaleImage when they were done.

  • GrayscaleImage crop(int startRow, int startCol, int newHeight, int newWidth) const

    Get a new GrayscaleImage that contains a "crop" from this image (don't actually modify the original). The crop should copy pixels starting from startRow and startCol into a new image that is newHeight by newWidth.

    Here are some sample crops of a 4x4 image:

    Crops of a 4x4 image

    There is a second test for this function to add error handling. If the crop is impossible because the pixels we would have to copy go outside of the boundaries of the original image, either by starting from a negative value or extending past the width or height, throw an out_of_range exception.

Optional Fun

The code you wrote can be used to work with real images. We just need some extra code to read and write the actual .bmp format used by real images. That code is provided in ImgIO.h and ImgIO.cpp. Download them and add them to a new regular project (not unit test) along with your GrayscaleImage.h/.cpp and this main.cpp. Then update the HEADERS and PROGRAM_FILES lines in the Makefile to list all of the .h and .cpp files.

Then add this cat.bmp file ( cat credits wikimedia commons) to the project's working directory and run it.

It should use your code to modify the cat picture in various ways!