DEV Community

Cover image for CS50 PSet Filter(more) : filter.c
vivekvohra
vivekvohra

Posted on • Updated on

CS50 PSet Filter(more) : filter.c

In this post, we continue our series with a more detailed look at the filter.c file, while breaking down its key components and functionality.

The filter.c file is part of the "Filter" problem set in CS50. It's a program that applies filters to bitmap images. The file contains a main function that handles the command-line arguments and calls the appropriate filter function. The actual implementation of the filters is done in the helpers.c file.

Now, when we open up filter.c. We see that this file has already been written for us, but it's still important to know the inner workings of this code.

Now this code roughly performs these functions:

  • Firstly it handles the command-line arguments, checks for correct usage, and opens the input and output image files.

  • Then it reads the bitmap file into a 2D array of RGBTRIPLEs, which is a data structure defined in the bmp.h header file. Each RGBTRIPLE represents a pixel in the image.

  • Then, it calls the appropriate filter function based on the command-line arguments. Which is written in helpers.c file, which we write as part of the problem set.

  • After the filter has been applied to the image, it writes the 2D array back to a bitmap file, creating the output image.

Now let us discuss the code in detail :

Ensure Proper Syntax of CLA.

Command-Line Filter Flag Validation

    // Define allowable filters 
    char *filters = "begr";

    // Get filter flag and check validity
    char filter = getopt(argc, argv, filters);
    if (filter == '?')
    {
        printf("Invalid filter.\n");
        return 1;
    }

Enter fullscreen mode Exit fullscreen mode
  • char *filters = "begr"; This line defines the filters that are allowed in the command line.
  • char filter = getopt(argc, argv, filters); This line calls the getopt function, which analyzes the command-line arguments. The arguments for this function are argc, argv, and the string of allowable filters.
  • if (filter == '?') This line checks if the returned value from getopt is '?'. If getopt encounters a character that is not included in the allowable options, it returns '?'.

Example:

The arguments are filters that can be 'b', 'e', 'g', or 'r'.

Let the command be: ./program -b

In this case, argc would be 2 (the program name and the '-b' argument), and argv would be an array containing "./program" and "-b".

When getopt(argc, argv, filters) is called, it will** return the character 'b'** because '-b' is a valid command-line argument according to the filters string. So, filter would be 'b'.

If the command is: ./program -xgetopt would return '?' because '-x' is not a valid argument according to filters, and filter would be '?'.

Checking for Multiple Filter Flags

    if (getopt(argc, argv, filters) != -1)
    {
        printf("Only one filter allowed.\n");
        return 1;
    }
Enter fullscreen mode Exit fullscreen mode

The getopt function is called again. If it returns anything other than -1, it means there's another command-line argument present. -1 is returned by getopt when it has finished processing all the command-line options. 

Example 1 : input ./program -b infile outfile, the code recognizes '-b' as a valid filter and confirms that there is only one filter provided.
Example 2: if we run your program: ./program -b -ggetopt will not return -1 after processing -b because there's another argument -g. The 'if' condition will be true, and the program will print "Only one filter allowed." and return 1, indicating an error.

This code ensures that only one filter can be used at a time. If more than one is provided, it's considered an error.

Ensure infile, outfile in syntax

    // Ensure proper usage
    if (argc != optind + 2)
    {
        printf("Usage: ./filter [flag] infile outfile\n");
        return 1;
    }
Enter fullscreen mode Exit fullscreen mode
  • argc is the count of command-line arguments, including the program name. optind is a variable from the getopt library that represents the index of the next argument to be processed.

The condition argc != optind + 2 checks if the number of arguments is not equal to the number of already processed arguments (optind) plus 2. The '+2' accounts for the 'infile' and 'outfile' that should follow the filter flag.

For example, if we run program: ./program -b infile outfile, the value of  argc would be 4, and of  optind would be 2 (after processing '-b'), therefore value of  optind + 2 would be 4. Thus the condition would be false, and the program would skip.

If we run program: ./program -b infile, the value of argc would be 3, and the value of optind would still be 2, but the value of optind + 2 would be 4. Thus the condition would be true, the program would print "Usage: ./filter [flag] infile outfile", and return 1, indicating an error.

Loading Files

Assigning Input and Output File Names

    // Assigning Input and Output File Names from Command-Line Arguments
    char *infile = argv[optind];
    char *outfile = argv[optind + 1];
Enter fullscreen mode Exit fullscreen mode
  • ./program -b infile outfile, argc :  optind is the index of the next argument to be processed by getopt.argv[optind] would be the argument right after the last processed option, which should be the input file name ('infile') in this case.

Opening Input and Output File

    // Open input file
    FILE *inptr = fopen(infile, "r");
           -----

        // Open output file
    FILE *outptr = fopen(outfile, "w");
          -----

    // Read infile's BITMAPFILEHEADER
    BITMAPFILEHEADER bf;
    fread(&bf, sizeof(BITMAPFILEHEADER), 1, inptr);

    // Read infile's BITMAPINFOHEADER
    BITMAPINFOHEADER bi;
    fread(&bi, sizeof(BITMAPINFOHEADER), 1, inptr);
Enter fullscreen mode Exit fullscreen mode

The above code opens the input/output file; and returns 1 if the file is not found. Else it reads the header files from bmp.h.

Validating BMP File Format

    // Ensure infile is (likely) a 24-bit uncompressed BMP 4.0
    if (bf.bfType != 0x4d42 || bf.bfOffBits != 54 || bi.biSize != 40 ||
        bi.biBitCount != 24 || bi.biCompression != 0)
    {
        fclose(outptr);
        fclose(inptr);
        printf("Unsupported file format.\n");
        return 6;
    }
Enter fullscreen mode Exit fullscreen mode
  • bf.bfType != 0x4d42 checks if the file type is not 'BM' (0x4d42 is the hexadecimal representation of 'BM').

  • bf.bfOffBits != 54 checks if the offset to the bitmap data is not 54 bytes.

  • bi.biSize != 40 checks if the size of the info header is not 40 bytes.

  • bi.biBitCount != 24 checks if the bitmap is not 24-bit.

  • bi.biCompression != 0 checks if the bitmap is not uncompressed.

If any of these conditions are true, it means the file is not a 24-bit uncompressed BMP 4.0 file

Get image's dimensions

// Get image's dimensions
    int height = abs(bi.biHeight);
    int width = bi.biWidth;
Enter fullscreen mode Exit fullscreen mode

This code retrieves the dimensions of the image from the bitmap info header.

bi.biHeight gives the height of the image in pixels. It can be negative, depending on the orientation of the image. Therefore we
take the absolute value to ensure the height is always positive.

bi.biWidth (defined in bmp.h file )gives the width of the image in pixels.

For example, if the bitmap image is 800 pixels wide and 600 pixels high, bi.biWidth would be 800 and bi.biHeight would be 600 (or -600, depending on orientation), so height would be 600 and width would be 800.

Memory allocation for the image

    // Allocate memory for image
    RGBTRIPLE(*image)[width] = calloc(height, width * sizeof(RGBTRIPLE));
    if (image == NULL)
    {
        printf("Not enough memory to store image.\n");
        fclose(outptr);
        fclose(inptr);
        return 1;
    }
Enter fullscreen mode Exit fullscreen mode

This code allocates memory to store the image data.

RGBTRIPLE(*image)[width] = calloc(height, width * sizeof(RGBTRIPLE)); is declaring a pointer to an array of RGBTRIPLEs (which represent pixels) and allocating memory for height rows of width pixels each. calloc initializes the allocated memory to zero.

If calloc returns NULL.It means there was not enough memory to store the image, so the program prints an error message, closes the input and output files, and returns 1.

For example, if your image is 800 pixels wide and 600 pixels high, this code would allocate memory for 600 rows of 800 RGBTRIPLEs each. If there is not enough memory to do this, the program would print "Not enough memory to store image." and terminate.

Padding

    // Allocate memory for image
    RGBTRIPLE(*image)[width] = calloc(height, width * sizeof(RGBTRIPLE));
    if (image == NULL)
    {
        printf("Not enough memory to store image.\n");
        fclose(outptr);
        fclose(inptr);
        return 1;
    }
Enter fullscreen mode Exit fullscreen mode

This line of code is calculating the amount of padding needed for each row (scanline) of the image.

For example: If the Image is 5 pixels (15 bytes) wide(it would need 1 byte of padding in each row).So that it can be 16 bytes wide.

BGR BGR BGR BGR BGR 0
BGR BGR BGR BGR BGR 0
BGR BGR BGR BGR BGR 0
BGR BGR BGR BGR BGR 0
BGR BGR BGR BGR BGR 0

Each row must be a multiple of 4 bytes in a bitmap file. If the width of the image in bytes (width * sizeof(RGBTRIPLE)) is not a multiple of 4, some padding bytes (extra bytes of value 0) need to be added to the end of each row to make it a multiple of 4.

(width * sizeof(RGBTRIPLE)) % 4 calculates the remainder when the width of the image in bytes is divided by 4. If this is 0, no padding is needed. Else 4 - (width * sizeof(RGBTRIPLE)) % 4 calculates how many bytes of padding are needed. The % 4 at the end is to handle the case where no padding is needed (it ensures the padding is 0 in this case).

For example, if the image is 5 pixels (15 bytes) wide, (width * sizeof(RGBTRIPLE)) % 4 would be 3, so 4 - (width * sizeof(RGBTRIPLE)) % 4 would be 1, meaning 1 byte of padding is needed for each row. If your image is 4 pixels (12 bytes) wide, (width * sizeof(RGBTRIPLE)) % 4 would be 0, so 4 - (width * sizeof(RGBTRIPLE)) % 4 would be 4, but the % 4 at the end would make the padding 0 .

Why do padding?

The requirement for BMP rows to be a multiple of 4 bytes is due to the way computers handle data.

Computers often read data from memory in chunks of 4 bytes (32 bits) at a time. By ensuring each row aligns with these 4-byte boundaries, the computer can read the image data more efficiently.

This is known as "byte alignment" or "data structure alignment".

If the rows weren't padded to be a multiple of 4 bytes, the computer might need extra read operations, which could slow down the process of reading the image data.

Reading Pixels from BMP File

    // Iterate over infile's scanlines
    for (int i = 0; i < height; i++)
    {
        // Read row into pixel array
        fread(image[i], sizeof(RGBTRIPLE), width, inptr);

        // Skip over padding
        fseek(inptr, padding, SEEK_CUR);
    }
Enter fullscreen mode Exit fullscreen mode

This code reads the image data from a BMP file.

The for loop iterates over each row of the image. For each row, fread(image[i], sizeof(RGBTRIPLE), width, inptr); reads width pixels from the input file into the image[i] array.

fseek is a function that is used to change the position of the file pointer in a file. It takes three arguments: a file pointer, an offset (number of bytes), and a position from where the offset is added.

In fseek(inptr, padding, SEEK_CUR);inptr is the file pointer, padding is the offset (is the number of bytes that the file pointer should move from its current position.), and SEEK_CUR is the position from where the offset is added.

SEEK_CUR is a constant defined in the library that specifies that the offset provided should be added to the current position of the file pointer.

So, fseek(inptr, padding, SEEK_CUR); moves the file pointer padding bytes forward from its current position. This is used to skip over the padding bytes at the end of each row of the image. If there is no padding, the file pointer doesn't move.

Filter

    switch (filter):  
Enter fullscreen mode Exit fullscreen mode

The above code calls the appropriate filter function based on the command-line arguments. The actual implementation of the filters is done in the helpers.c file.

save

After the filter has been applied to the image, the rest of the code writes the 2D array back to a bitmap file, creating the output image using fwrite function.

  • In the end, we free up the memory that was previously allocated to the image variable and close the input and output files that were previously opened.

In the next blog post, we will discuss about helper.c file. See you there! In the meantime continue to code with passion. 🚀

As a novice writer, I’m eager to learn and improve. Should you spot any errors, I warmly welcome your insights in the comments below. Your feedback is invaluable to me.

My code :

GitHub logo vivekvohra / filter

This code requires <cs50 library> to run.

filter

In this problem, we have to code the following filter:

  • Grayscale function takes an image and turns it into a black-and-white version of the same image.

  • Reflect function takes an image and reflects it horizontally.

  • Blur function takes an image and turns it into a box-blurred version of the same image.

  • Edge function takes an image and highlights the edges between objects, according to the Sobel operator.

Grayscale:

This function takes an image and converts it into a black-and-white version of the same image. This is done by taking an average of the RGB values of each pixel and setting them all equal to the average.

Reflect:

This function flips an image about the vertical axis, which returns a mirror image.

Blur:

The purpose here is to return a blurred version of the input image. We do this by implementing the “box blur,” which works by taking each pixel…




Top comments (0)