Project 3: Image Processing

Overview

After the second project sort of turning into a hungry monster, this third one is going to seem almost relaxing. Still, since it involves fiddling with file formats that are “weakly specified” at best, you’ll probably want to start right away again.

We’re going into a completely different domain this time, namely image processing. However, since this course is obviously about programming in C and not about image processing, we’re staying away from complex and math-heavy operations. So that’s good news.

There are once again two parts to this project: First you’re going to implement a module image that will allow you to represent, manipulate, load, and store images. You’ll get an image.h header file on Piazza and you’ll write the image.c implementation for it. Of course you’ll also develop unit tests, do coverage analysis, all that good stuff. This time, we’ll even give you some basic test drivers to start with…

Second you’re going to develop a small set of command-line tools for image processing. These tools will use the image module from above, but you may also want to develop a second module imageops that collects support code focusing on “higher-level” image operations that several of your tools can use. Together, these tools will allow you to perform some rather silly image processing tasks on some guy called Donald.

As always, you’re expected to develop (and submit!) a useful testing infrastructure for your project. This will involve system testing (for each command-line tool) and unit testing (certainly for image.c but also for imageops.c if you decide to develop such a module).

The biggest challenge for this project will probably be dealing with the required file format. Try to make sure that your function(s) for loading images can deal with all the strange features allowed by the format. Your function(s) for saving images, on the other hand, should produce very clean output files without any strange exceptions.

Image Processing Tools

You will develop three small image processing tools called invert, merge, and interlace. We’ll demonstrate what these tools need to do starting with the following example image:

donald-256x256.ppm

Note that here on the handout we use JPEG images so the file above is donald-256x256.jpg if you were to save it. However, reading and writing JPEG images is way too complicated for us to implement. So for the project we use a simpler format called PPM which is why the following commands all refer to PPM files.

If we run the invert command using ./invert donald-256x256.ppm inverted-256x256.ppm, the resulting image file would look as follows:

inverted-256x256.ppm

In images like these, the color of each pixel is represented by three values for the red, green, and blue components. Each of these can be between 0 and 255: 0 means complete absence of the component in question, 255 means complete presence. So “black” has red 0, green 0, blue 0 whereas “white” has red 255, green 255, blue 255. The invert program takes the color of each pixel and replaces it with it’s “opposite” color by subtracting each component from 255.

Now let’s look at the merge program. Say we have another image that looks like this:

gradient-256x256.ppm

If we run the merge command using ./merge donald-256x256.ppm gradient-256x256.ppm merged-256x256.ppm, the resulting image file would look as follows:

merged-256x256.ppm

Here we generated a new image by averaging the colors from the two source images component by component. Obviously the source images must have the same size for this to work properly.

Finally let’s look at the interlace program. Given the original image and the inverted image we produced above, running the command ./interlace donald-256x256.ppm inverted-256x256.ppm interlaced-256x256.ppm results in the following image:

interlaced-256x256.ppm

Here we took the even lines (0, 2, 4, …) from the first image and the odd lines (1, 3, 5, …) from the second image to produce a scary mess. Of course we can then merge the gradient on top if we really want to:

monster-256x256.ppm

And so on, and so forth. The invert, merge, and interlace programs are required for this project, but you’re free (and indeed invited!) to write more tools that do more interesting things if you feel like it. Playing with images can be a fun hobby after all…

Refactoring

As you write the image processing tools, you’ll realize that each of them has two major parts: One in which you deal with command line arguments as well as loading and saving images, and one in which you implement the actual image processing operation. It would probably be a good idea to collect the image processing stuff into a module imageops so that the code can be reused from other C programs directly. Introducing such a module is highly recommended.

The Image Module

You’ll get an image.h header file on Piazza and you’ll write the image.c implementation for it. If you read through the header, you’ll see that there are four big things the image module provides:

Taken together, that’s exactly what you’ll need to do image processing but no more. The module provides just the “bare minimum” of functionality to work with images. Except for the file format used to load and save images, most of the code for image.c should be obvious just from the header.

File Format

We’ll use a simple file format called PPM to load and save images. There are actually two versions of PPM, one that uses plain ASCII files and another that uses binary files. We’ll use the plain ASCII version to keep things simple. You can find out more about this file format here:

Just in case it’s not obvious, we’re talking about the P3 format and the P3 format only. On Piazza we’ll provide some examples of the file format, but you must be careful: The specification allows for more variation than what our examples show. Implementing the load_image function correctly, according to the specification, requires quite a bit of effort. Your load_image should be able to load any valid PPM file, not just the PPM files we provide. Your best bet is to go “hunting” for more exotic examples online…

Sample Test Drivers

On Piazza we’ll provide two sample test drivers. You will certainly want to do more extensive testing than what they provide, but at least they’ll help you get started. The test_orientation test driver is particularly important: It allows you to check that your set_pixel and get_pixel functions are using the coordinate system correctly. Here’s what the output image should look like:

test_orientation.ppm

Wait, that’s too hard to see. Here it is again, zoomed in this time:

test_orientation-large.ppm

If you’re not sure what this is about, please read the comments in both image.h and test_orientation.c for more details.

Testing

As always, you should have a testing infrastructure in place for your project. Since there are two distinct parts, the image modules (and possibly the imageops module as well) and the image processing tools, there will (once again) be two distinct approaches to testing:

You’ll also have to perform coverage analysis for your test cases. That is, you’ll have to use gcov to determine what parts of your code base your tests cases actually test. Your goal is to get 100% line coverage for each source file, but it may not be realistic to achieve that; try to get as close as possible and defend any missing coverage in your README file.

Hints

Deliverables

All your core C code should be in image.c, invert.c, merge.c, and interlace.c. If you decide on an imageops module, you should have imageops.h and imageops.c as well. For the required parts of this project, please don’t write additional modules that complicate how your program must be linked! If you include any additional image processing tools, it’s up to you how you structure and build them. But you should be consistent!

Please follow the submission instructions as detailed on Piazza. Make sure that your tarball contains no derived files whatsoever (i.e. no executable files), but allows building all required derived files. Also, be sure to include a Makefile that sets the appropriate compiler flags and builds all programs by default. The Makefile should also have clean and test targets as per usual; the test target should run both system and unit tests; ideally it also runs coverage analysis for you. Finally, make sure to include your name and email address in every file you turn in (well, in every file for which it makes sense to do so anyway)!

Grading

For reference, here is a short explanation of the grading criteria; some of the criteria don’t apply to all problems, and not all of the criteria are used on all assignments.

Packaging refers to the proper organization of the stuff you hand in, following both the guidelines for Deliverables above as well as the general submission instructions for assignments on Piazza.

Style refers to C programming style, including things like consistent indentation, appropriate identifier names, useful comments, suitable documentation, etc. Simple, clean, readable code is what you should be aiming for. Make sure you follow the style guide posted on Piazza!

Design refers to proper modularization (functions, modules, etc.) and an appropriate choice of algorithms and data structures.

Performance refers to how fast/with how little memory your programs can produce the required results compared to other submissions.

Functionality refers to your programs being able to do what they should according to the specification given above; if the specification is ambiguous, ask for clarification! (It also refers to you simply doing the required work, which may not be programming alone.)

If your programs cannot be built you will get no points whatsoever. If your programs cannot be built without warnings using the required compiler options given on Piazza we will take off 10% (except if you document a very good reason). If your programs cannot be built using make we will take off 10%. If your programs fail miserably even once, i.e. terminate with an exception of any kind or dump core, we will take off 10% (for each such case).