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.
Here is the same image (a heart) shrunk down:
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:
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_t
s. 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 useto_string(num)
to convert numbers to strings.Overriding the
<<
operator is the more idiomatic C++ way of doing output:cout << image;
instead ofcout << 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 thepixels
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)
andGrayscaleImage& 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 andbrightness
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).
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
andstartCol
into a new image that isnewHeight
bynewWidth
.Here are some sample 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
orheight
, throw anout_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!