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;
}
-
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 -x
, getopt
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;
}
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 -g
, getopt
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;
}
-
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];
- ./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);
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;
}
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;
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;
}
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 RGBTRIPLE
s (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 RGBTRIPLE
s 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;
}
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);
}
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):
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 :
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.
For detailed explanation click on link below :
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…
Top comments (0)