Image filters in C

Another task in the CS50’s problem set 4 was to apply filters to BMP images. Luckily, the core functionality was provided by the staff, so I had to only implement four functions: grayscale, sepia, reflect, and blur.

Let me share my walkthrough for each function.

Grayscale

First, I need to loop through each pixel in the image. I do this by using two nested loops, one for the height and another for the width, to access each pixel individually:

for (int i = 0; i < height; i++)
{
    for (int j = 0; j < width; j++)
    {
        // Pixel processing will be done here
    }
}

For each pixel, I calculate the average value of its red, green, and blue components. I add up the red, green, and blue values and divide the sum by 3.0 to get the average.

int average =
    round((image[i][j].rgbtRed + image[i][j].rgbtGreen + image[i][j].rgbtBlue) / 3.0);

This new average value represents the intensity of the grayscale colour for that pixel.

Why do I divide by 3.0 instead of 3, you may ask? It’s a decision related to data types and precision. When you divide by an integer (e. g., 3), the result will also be an integer, potentially causing a loss of precision in the calculation.

By using 3.0, we explicitly specify that we want floating-point division. In C, if at least one of the operands in a division operation is a floating-point number (like 3.0), the result will be a floating-point number. This is important for obtaining a more accurate average, especially when dealing with colour values that might have fractional parts after the division.

Consider this example:

int result_int = 5 / 3;    // Result is 1, as it's integer division
float result_float = 5 / 3.0;  // Result is 1.66667, as it's floating-point division

Now, I update the red, green, and blue values of the pixel with the calculated average. This makes all three colour components of the pixel equal, resulting in a grayscale effect:

image[i][j].rgbtRed = average;
image[i][j].rgbtGreen = average;
image[i][j].rgbtBlue = average;

And since these steps are inside the nested loop, the entire image is converted to grayscale.

Here is my full code and the result:

// Convert image to grayscale
void grayscale(int height, int width, RGBTRIPLE image[height][width])
{
    // Loop over all pixels
    for (int i = 0; i < height; i++)
    {
        for (int j = 0; j < width; j++)
        {
            // Take average of red, green, and blue
            // Take average of red, green, and blue
            int average =
                round((image[i][j].rgbtRed + image[i][j].rgbtGreen + image[i][j].rgbtBlue) / 3.0);

            // Update pixel values
            image[i][j].rgbtRed = average;
            image[i][j].rgbtGreen = average;
            image[i][j].rgbtBlue = average;
        }
    }

    return;
}
Grayscale filter Original image

Sepia

Similar to the grayscale function, I start by looping through each pixel in the image using nested loops for height and width:

for (int i = 0; i < height; i++)
{
    for (int j = 0; j < width; j++)
    {
        // Pixel processing will be done here
    }
}

I extract the original red, green, and blue values of the current pixel to variables for easier manipulation:

int original_red = image[i][j].rgbtRed;
int original_green = image[i][j].rgbtGreen;
int original_blue = image[i][j].rgbtBlue;

Using specific weighted averages, I calculate new values for red, green, and blue to achieve the sepia tone effect. The formula involves multiplying each original colour component by a specific constant and summing them up:

int sepia_red = (int)fmin(
    255, round(0.393 * original_red + 0.769 * original_green + 0.189 * original_blue));
int sepia_green = (int)fmin(
    255, round(0.349 * original_red + 0.686 * original_green + 0.168 * original_blue));
int sepia_blue = (int)fmin(
    255, round(0.272 * original_red + 0.534 * original_green + 0.131 * original_blue));

Note that I use fmin(255, ...) to cap the values at 255 to ensure they don’t exceed the maximum intensity.

Finally, I update the red, green, and blue values of the pixel with the calculated sepia values:

image[i][j].rgbtRed = sepia_red;
image[i][j].rgbtGreen = sepia_green;
image[i][j].rgbtBlue = sepia_blue;

Here is my full code and the result:

// Convert an image to sepia
void sepia(int height, int width, RGBTRIPLE image[height][width])
{
    // Loop over all pixels
    for (int i = 0; i < height; i++)
    {
        for (int j = 0; j < width; j++)
        {
            // Compute sepia values
            int original_red = image[i][j].rgbtRed;
            int original_green = image[i][j].rgbtGreen;
            int original_blue = image[i][j].rgbtBlue;

            // Calculate sepia values, rounding to the nearest integer and capping at 255
            int sepia_red = (int) fmin(
                255, round(0.393 * original_red + 0.769 * original_green + 0.189 * original_blue));
            int sepia_green = (int) fmin(
                255, round(0.349 * original_red + 0.686 * original_green + 0.168 * original_blue));
            int sepia_blue = (int) fmin(
                255, round(0.272 * original_red + 0.534 * original_green + 0.131 * original_blue));

            // Update pixel with sepia values
            image[i][j].rgbtRed = sepia_red;
            image[i][j].rgbtGreen = sepia_green;
            image[i][j].rgbtBlue = sepia_blue;
        }
    }
}
Sepia filter Original image

Reflect

For this function, I start by looping through each row of the image using a single loop for the height:

for (int i = 0; i < height; i++)
{
    // Row processing will be done here
}

Within each row, I use two pointers, start and end, to swap pixels from the outer edges towards the centre. The start pointer begins at the first pixel (index 0), and the end pointer begins at the last pixel:

int start = 0;
int end = width - 1;

I use a while loop to continue swapping pixels until the start pointer is no longer less than the end pointer. Within the loop, I use a temporary variable to do the swapping:

while (start < end)
{
    // Swap pixels using a temporary variable
    RGBTRIPLE temp = image[i][start];
    image[i][start] = image[i][end];
    image[i][end] = temp;

    // Move the pointers towards the centre
    start++;
    end--;
}

The entire process is repeated for each row of the image, making the reflection apply to the whole image.

Here is my full code and the result:

// Reflect image horizontally
void reflect(int height, int width, RGBTRIPLE image[height][width])
{
    // Loop over all rows
    for (int i = 0; i < height; i++)
    {
        // Use two pointers to swap pixels from the outer edges towards the centre
        int start = 0;
        int end = width - 1;

        while (start < end)
        {
            // Swap pixels using a temporary variable
            RGBTRIPLE temp = image[i][start];
            image[i][start] = image[i][end];
            image[i][end] = temp;

            // Move the pointers towards the centre
            start++;
            end--;
        }
    }
}
Reflected image Original image

Blur

So, first, I create a temporary image to store the blurred result. This temporary image has the same dimensions as the original image:

RGBTRIPLE temp[height][width];

I then loop through each pixel in the original image using nested loops for height and width:

for (int i = 0; i < height; i++)
{
    for (int j = 0; j < width; j++)
    {
        // Pixel processing will be done here
    }
}

For each pixel, I initialize variables to calculate the average colour values: red_sum, green_sum, blue_sum, and count. These variables will be used to accumulate the colour values of neighbouring pixels:

int red_sum = 0;
int green_sum = 0;
int blue_sum = 0;
int count = 0;

Next, I use nested loops to iterate over a 3x3 grid centred around the current pixel. I use row and col to determine the position of the neighbouring pixels relative to the current pixel:

for (int row = -1; row <= 1; row++)
{
    for (int col = -1; col <= 1; col++)
    {
        // Neighboring pixel calculation will be done here
    }
}

For each neighbouring pixel, I calculate its position using newRow and newCol variables. I then check if it is within the bounds of the image. If it is, I accumulate the colour values (red_sum, green_sum, and blue_sum) and increment the count variable:

int newRow = i + row;
int newCol = j + col;

if (newRow >= 0 && newRow < height && newCol >= 0 && newCol < width)
{
    red_sum += image[newRow][newCol].rgbtRed;
    green_sum += image[newRow][newCol].rgbtGreen;
    blue_sum += image[newRow][newCol].rgbtBlue;
    count++;
}

After iterating over the neighbouring pixels, I calculate the average colour values for the current pixel using the accumulated sums and the count of valid neighbouring pixels and use the round function to round the result to the nearest integer:

temp[i][j].rgbtRed = round((float)red_sum / count);
temp[i][j].rgbtGreen = round((float)green_sum / count);
temp[i][j].rgbtBlue = round((float)blue_sum / count);

Once all pixels have been processed and the temporary image contains the blurred result, I copy the contents of the temporary image back to the original image:

for (int i = 0; i < height; i++)
{
    for (int j = 0; j < width; j++)
    {
        image[i][j] = temp[i][j];
    }
}

I must admit, this one was a bit tough, and it took me some time to figure out!

Here is my full code and the result:

// Blur image
void blur(int height, int width, RGBTRIPLE image[height][width])
{
    // Create a temporary image to store the blurred result
    RGBTRIPLE temp[height][width];

    // Loop over all pixels
    for (int i = 0; i < height; i++)
    {
        for (int j = 0; j < width; j++)
        {
            // Initialize variables to calculate the average colour values
            int red_sum = 0;
            int green_sum = 0;
            int blue_sum = 0;
            int count = 0;

            // Loop over neighboring pixels (3x3 grid centered around the current pixel)
            for (int row = -1; row <= 1; row++)
            {
                for (int col = -1; col <= 1; col++)
                {
                    int newRow = i + row;
                    int newCol = j + col;

                    // Check if the neighboring pixel is within the image bounds
                    if (newRow >= 0 && newRow < height && newCol >= 0 && newCol < width)
                    {
                        // Accumulate colour values
                        red_sum += image[newRow][newCol].rgbtRed;
                        green_sum += image[newRow][newCol].rgbtGreen;
                        blue_sum += image[newRow][newCol].rgbtBlue;
                        count++;
                    }
                }
            }

            // Calculate average colour values
            temp[i][j].rgbtRed = round((float) red_sum / count);
            temp[i][j].rgbtGreen = round((float) green_sum / count);
            temp[i][j].rgbtBlue = round((float) blue_sum / count);
        }
    }

    // Copy the blurred image back to the original image
    for (int i = 0; i < height; i++)
    {
        for (int j = 0; j < width; j++)
        {
            image[i][j] = temp[i][j];
        }
    }
}
Blurred image Original image
 89   1 mo   C   CS50   Programming
Next
© Daniel Sokolovskiy, 2024
Powered by Aegea