Lab 2: PicFilter
Due on Sunday, February 7th at 11:59 PM. This is an individual lab. You are to complete this lab on your own, although you may discuss the lab concepts with your classmates. Remember the Academic Integrity Policy: do not show your code to anyone outside of the course staff and do not look at anyone else’s code for this lab. If you need help, please post on the Piazza forum or contact the instructor or ninjas. If you have any doubts about what is okay and what is not, it’s much safer to ask than to risk violating the Academic Integrity Policy.
Overview
In this lab, you will write a program to apply filters to a particular image file format. You will become familiar with the following in C++:
- Command-line arguments
- File I/O
- Organizing your code into separate files
You will also write a series of tests to verify the correctness of your program without running it directly. As with the previous lab, you should clone your repository; the URL is git@github.swarthmore.edu:cs35-s16/lab02-<your-username>
.
The PPM Image Format
PPM is an image file format similar to JPG/JPEG, PNG, GIF, and so on. PPM is less frequently used because it is very inefficient: a PPM file can easily be ten times larger than a JPG containing the same picture because a JPG file uses clever compression algorithms. We will be using PPM in this lab because, in comparison to other image formats, it is very simple.
The PPM format we are using is really just a text file. The file starts by giving some simple information: a “magic number” identifying the PPM format (which helps us be sure that we’re actually editing a PPM file and not junk), the height and width of the PPM file, a maximum color value, and then a series of numbers to describe the pixels of the image. For instance, suppose a file square.ppm
contained the following. (You could even create this file in a text editor if you wanted.)
P3
4 4
255
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 255 255 255 255 255 255 0 0 0
0 0 0 255 255 255 255 255 255 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
The above file is broken down as follows:
P3
is the magic number that tells us that this is a text-based PPM image file.4 4
tells us that this image is 4 pixels wide by 4 pixels tall.255
tells us that 255 is the largest number that will appear in any pixel; this number should always be between 1 and 255.0 0 0
tells us that a pixel has0
red,0
green, and0
blue (so it is black).255 255 255
tells us that a pixel has255
red,255
green, and255
blue (so it is white).
Here is another example PPM file, slash.ppm
:
P3
5 3
255
0 0 255 0 0 255 0 0 255 0 0 0 255 128 0
0 0 255 0 0 255 0 0 0 255 128 0 255 128 0
0 0 255 0 0 0 255 128 0 255 128 0 255 128 0
This time, the image is five pixels wide by 3 pixels tall. The pixel in the upper left has 0
red, 0
green, 0
blue (which displays as a bright blue), while the pixel in the lower right has 255
red, 128
green, and 0
blue (which appears as orange).
The PicFilter Program
You will write a program called picfilter
which allows the user to manipulate PPM files from the command line. Your program will read a PPM file into memory as an object, perform transformations on it, and then save it back to disk in another PPM file. Your program will take the input file, the output file, and all of the transformations to perform as command-line arguments. For instance,
./picfilter old.ppm new.ppm flip-horizontal
will read the file old.ppm
, flip it horizontally (left to right, as in a mirror), and then save the result as new.ppm
. The user is permitted to specify multiple transformations, so
./picfilter old.ppm new.ppm flip-horizontal grayscale
will flip the image horizontally and then apply a grayscale filter (to make the image appear “black and white”). The operations you will be required to perform are as follows; the example text for each of them assumes that 255
is the maximum pixel value.
no-red
: All red values are set to0
.no-green
: All green values are set to0
.no-blue
: All blue values are set to0
.invert
: All channels are subtracted from the maximum pixel value. For instance,255 128 0
would become0 127 255
.grayscale
: The channels of each pixel are averaged. For instance, the pixel255 128 0
would become127 127 127
(since(255+128+0)/3
is approximately127
).flip-horizontally
: All pixels are moved across the X axis. The first pixel in each row becomes the last pixel in that row, the second pixel in each row becomes the second-to-last pixel in that row, and so on.flip-vertically
: All pixels are moved across the Y axis, causing the image to appear upside-down.
If the user gives an input file which doesn’t exist or gives a transformation which does not exist (e.g. flip-diagonal
), your program should generate an appropriate error message and quit.
Writing Your Program
Your starter code has been separated into several files:
image.cpp
: This file will contain a definition of thePpmImage
class. APpmImage
object represents an in-memory image at runtime.picfilter.cpp
: This file contains a function calledpicfilter_main
. It looks exactly like amain
function except for its name and you should treat it as themain
of your program. We separate it for testing purposes.program.cpp
: This file contains themain
function for yourpicfilter
program. It simply callspicfilter_main
. You will not need to modify this file, but you should understand what it is doing.tests.cpp
: This file contains unit tests of your code.test_utils.o
: This file contains tools your tests can use.
There are also header files – image.h
, picfilter.h
, and test_utils.h
– which you will not need to modify for this assignment. They describe the functions that their corresponding cpp
files implement.
The PpmImage
Class
The image.cpp
file contains a skeleton for a PpmImage
class. It includes one method for each of the above transformations; it also includes a constructor (which is used to load image files), a few methods (save
and position_of
), and a destructor. The image.h
file documents the expectation for each method. It’s important to note that the constructor for PpmImage
will throw an exception if it fails to read the image file correctly.
The PpmImage
class also defines fields for your class to use: width
, height
, max_value
, and pixel_values
. The pixel_values
field is an int*
which points to an array of integers; there are three integers for each pixel in the image so that a given pixel’s red, green, and blue are stored next to each other. You will write the position_of
method of PpmImage
, which will give the position of the red part of a pixel given its X and Y coordinates. This way, you can use the position_of
method from within your other methods to keep them simpler.
Compiling Your Program
Like the previous lab, this lab includes a Makefile
. You can compile your program by typing make
or make picfilter
; you can then run it with ./picfilter
. You can compile your tests by typing make tests
; you can then run them with ./tests
.
Testing Your Program
The initial repository for your assignment includes a directory test_data
which has several PPM files in it. There are two original images provided: Rose.ppm
and Gerbil.ppm
. The remaining PPM files are the result of the above-described transformations. For instance, Gerbil__no-red.ppm
should be the PPM file produced by running the no-red
filter on the Gerbil.ppm
file.
The tests.cpp
file includes test_utils.h
, which provides a function CHECK_PPM_EQUAL
. This function is similar to CHECK_EQUAL
except that it verifies that two PPM files are equal (that is, they have the same size and pixel values). When you write tests for your program, you can use CHECK_PPM_EQUAL
to verify the files that you have written. An example test has already been written for you so that you can see how CHECK_PPM_EQUAL
might be used.
test_utils.h
also includes a function RUN_MAIN
to run picfilter_main
for you. picfilter_main
is just a function, so you could call it yourself, but it’s a bit of a pain to create the char**
pointing to all of the arguments you want to provide. So we’ve provided RUN_MAIN
to make this easier for you; inside of your TEST
, you can just do something like this:
RUN_MAIN(4, "picfilter", "test_data/Gerbil.ppm", "test_output/Gerbil__no-red.ppm", "no-red");
Note that “picfilter” is the first argument given; recall that the first command-line argument is always the name of the program we are running and, since we’re calling the picfilter_main
function, we have to simulate that. Note also that the command-line arguments are just given as a sequence of strings above; the RUN_MAIN
function will take care of bundling them into an array for you. The above code does not perform any tests, though, so you’ll need to do that (with CHECK_PPM_EQUAL
) yourself.
Getting Started
Remember: you don’t have to write your whole program all at once. In fact, it’s often best to make some small amount of progress and write tests for that code. You don’t even have to start by writing code for picfilter_main
. Here are some steps you can follow to complete your program.
- Start by writing the constructor for
PpmImage
. That constructor takes anifstream&
of the file that should be read and initializes the object to the contents of that image. You’ll need to usenew
in this constructor to allocate an array to store your pixel data; the provided code assumes that you will be using a single-dimensional array ofint
s. Also write the destructor ofPpmImage
(which should be a lot easier, since it just has todelete
the pixel array). - Compile and run the unit tests. There aren’t very many right now, but you’ve been provided some example tests. The first test makes sure that you can load a PPM file without an exception. The second test makes sure that your
PpmImage
constructor throws an exception when given a file that is not a PPM. Do not write more code until these two tests pass! You won’t be able to make meaningful progress until you can read in the PPM file and write it back out again. (The third test – copying the PPM – will fail, but that’s okay; you haven’t written the code for that yet.) - Implement the
position_of
method for your class. The rest of your methods should call this method to deal with finding pixels in your array, since every pixel is composed of three integers. - Next, write the
save
method for your class. You should be able to load and save a file and wind up with the same picture. Once you’re finished, run the tests again. The last example test that you have been provided will load a picture, save it to another name, and then check that the two files are equal. This helps to show howCHECK_PPM_EQUAL
is used. - Now, write each of the methods of the
PpmImage
class one at a time. Each time you finish one, write an appropriate test for it usingCHECK_PPM_EQUAL
. - After you’ve finished writing your
PpmImage
class and are confident that it is working, you can writepicfilter_main
. This should be pretty easy: just use the command-line arguments to determine the file to load, the operations to perform, and the file to save. - Next, write tests for
picfilter_main
! Remember to useRUN_MAIN
as described above. The template separatesmain
frompicfilter_main
so that you can write tests that callpicfilter_main
and make sure it does the right thing. This way, you can use tests to be sure that you are handling command-line arguments correctly as well. - Once all of your tests pass, you should be able to run your program with no bugs or problems. If you discover a bug, try writing more tests to see if you can figure out what went wrong!
Coding Style Requirements
For this lab, you will be required to observe some good coding practices:
-
You should pick meaningful variable names.
// Good int* pixels = new int[size]; // Bad int* p = new int[size];
-
You should use correct and consistent indentation. Lines of code within a block (that is, surrounded by
{
and}
) should be indented four spaces further than the lines surrounding them.// Good if (condition) { cout << "Test" << endl; } // Bad if (condition) { cout << "Test" << endl; }
-
You should use a block whenever possible, even if it’s not necessary. This helps you avoid subtle or messy bugs in the future.
// Good if (condition) { cout << "Something" << endl; } // Bad if (condition) cout << "Something" << endl;
Creating Your Own PPM Files
You can use ImageMagick, a package installed on the CS network, together with picfilter
to edit your own images! Start by converting your picture to PPM format:
convert my_image.jpg -compress none my_image.ppm
Then, do anything you want with picfilter
:
./picfilter my_image.ppm my_new_image.ppm grayscale invert flip-horizontal
Finally, convert your new image back to some other format.
convert my_new_image.ppm my_image.jpg
There! You’ve just used your homework assignment to perform photo editing on your own files. :)
Summary
To summarize the requirements of this lab:
- Your program must perform all of the image transformations listed above.
- Your program must take its input from command-line arguments.
- You must gracefully handle situations in which the user provides invalid command-line arguments (incorrect number of arguments, bad transformation names, or non-existing files).
- Your code must respond gracefully when given files that are not PPM files (e.g. a text file containing “
P3 stuff
”). - You must write tests for each function/method you write.
- This includes
picfilter_main
.
- This includes