Assignment 2

5 minute read

Type: Individual Assignment
Due: before class on Wednesday, September 22, 2021

Prologue

This assignment is designed to give you elementary practice in the C programming language. In particular, we will apply our newfound knowledge of C to implement image transformations on a simple file format. Most images are encoded as PNGs or JPGs which are rather complicated file formats. In this assignment, we will use a simpler file format: PPM.

Overall, your task for this assignment is to write functions that read PPM files, write PPM files, and perform simple color transformations.

Put all of your code for this assignment in a file images.c.

PPM File Format

The file format we are using is the plain PPM file format which stores the image as a sequence of characters. Below is an example plain PPM file which encodes an image that consists of four pixels in a 2x2 grid.

P3
2 2
255
0 0 0
255 255 255
255 0 255
128 128 128

Here is a brief explanation of the file format:

  1. The first line of the file always consists of the “magic number” P3.
  2. The second line consists of two positive integers that are the width and the height of the image in pixels.
  3. The third line will always be 255 in this assignment. (It is the maximum RGB value in the following pixel data.)
  4. The rest of the file consists of a sequence of pixels. In this assignment, each line will contain exactly one pixel in the form r g b where r, g, and b are integers between 0 and 255. The total number of lines is width * height where width and height are the numbers on line 2.

Note: The plain PPM file format is a little more complex than this, but you may assume that PPM files are formatted exactly as above in this assignment.

The COLOR and IMAGE Structs

For this assignment, you must make use of the following two structs that will help us encode PPM images in a way that is easy to manipulate.

typedef struct {
    int red;
    int green;
    int blue;
} COLOR;

typedef struct {
    int width;
    int height;
    COLOR *raster;
} IMAGE;

Note that we can create a color in the following way:

COLOR c = {.red = 255, .green = 0, .blue = 0};

which creates a COLOR value that represents the color red.

Note that the IMAGE struct contains the width and height of an image, and the raster field is an array of COLORs that represents the pixels of the image. Note that the raster array is one dimensional—this is intentional so that all the pixels appear in a single slot of memory.

We provide the following helper functions for your convenience:

// Creates and returns a pointer to a new blank IMAGE
// with the given dimensions (allocated on the heap)
IMAGE *img_new(int width, int height) {
    IMAGE *img = malloc(sizeof(IMAGE));
    img->width = width;
    img->height = height;
    img->raster = malloc(sizeof(COLOR) * width * height);
    return img;
}

// Frees the memory of the given IMAGE
void img_free(IMAGE *img) {
    free(img->raster);
    free(img);
}

Part 1: Reading a PPM File

For this part, your task is to write a function that reads a PPM file and constructs an IMAGE from it. The prototype of the function must be:

IMAGE *img_read(const char *filename)

This function takes a string as a parameter that is the name of the PPM file to read and returns a pointer to an IMAGE that is constructed on the heap from that PPM file. The const keyword helps avoid accidentally modifying the string filename in the function.

Hints!

  1. Use the img_new function to create a blank image and then modify its raster of pixels directly with the file data.
  2. You’ll find the stdio.h file functions like fopen, fclose, fgets, and sscanf helpful.
  3. I recommend that you read the file one line at a time with fgets and then parse the line into appropriate integers using sscanf.
  4. Populate the raster of the IMAGE so that the pixels are in the same order as they appear in the PPM file.

Part 2: Writing a PPM File

For this part, your task is to write a function that writes an IMAGE to a file in the plain PPM format. The prototype of the function must be:

void img_write(IMAGE *img, const char *filename)

When you call img_write, it should write the contents of the img parameter to a file with the name filename. Remember that you’ll need to output text so that it looks something like the following:

P3
2 2
255
0 0 0
255 255 255
255 0 255
128 128 128

Hints!

  1. Use fprintf to print string-formatted data to a file.
  2. Remember that the first three lines are special!
  3. Loop over the raster data and print off the pixels in order.

Part 3: Grayscale Effect

For this part, you need to write a function that transforms an IMAGE by applying an effect to each pixel in the image. In particular, we will be doing a grayscale effect.

Given a pixel with RGB values $(r,g,b)$, we can turn it gray by taking the average of its components. For example, the new color with have all three components being the same: $(n,n,n)$ where

\[n=\frac{r+g+b}{3}\]

Ultimately, your goal is to write a function with the following prototype:

void img_gray(IMAGE *img)

The function ought to manipulate the pixels of img “in place” so that every pixel is turned into its grayscale equivalent.

Testing

For testing purposes, you can use the main function below.

int main(int argc, char const *argv[])
{
    if (argc == 3) {
        IMAGE *img = img_read(argv[1]);
        img_gray(img);
        img_write(img, argv[2]);
        img_free(img);
        return 0;
    } else {
        printf("Error! Needs exactly two arguments.\n");
        return 1;
    }
}

The main function above takes two command line arguments: one for the input filename and one for the output filename. It uses img_read to read the input file, then img_gray to turn it grayscale, and then img_write to write the modified image to a new file.

Here is a cat image PPM file that you can use as a testing input file. It should look like the following:

cat image

You can run your program by doing the following:

$ gcc images.c
$ ./a.out cat.ppm gray_cat.ppm

The gray cat image created should look something like:

gray cat image