Modifying Images in C

Next up in my CS50 psets was a fun project working with files in C. First up was a challenge to find a hidden clue in a picture. You had to check the RGB values of each pixel in an image and remove the red pixels to reveal the person hidden underneath.

This program dealt with Bitmap images, which meant there was plenty to figure out with file types and file headers, but once you get the hang of it, it's not too hard to understand and reveals a lot about how your operating system figures out what file types it's using.

Working with Files

First you have to open up some files to work with. There is a lot of code in these excercises, so I'm going to try and keep the examples to the interesting/new parts.

FILE* inptr = fopen(infile, "r");
FILE* outptr = fopen(outfile, "w");

Basically we are given strings on the command line, and we are storing some pointers to addresses in memory where these files live. The "r" stands for "read" and the "w" stands for write. You could also append to a previously existing file, but since we're making copies at this point that's not needed.

File Headers

Then we get into some new territory that I never realized before: files like images have header information! I mean it makes sense when you think about it, especially so programs know what to do with different file types, but I never realized what that would include.

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

// read infile's BITMAPINFOHEADER
BITMAPINFOHEADER bi;
fread(&bi, sizeof(BITMAPINFOHEADER), 1, inptr);

// 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);
    fprintf(stderr, "Unsupported file format.\n");
    return 4;
}

So we read in the two headers and make sure they all match what we are expecting. If you're curious to learn more about bitmap file headers, MSDN has some great reference docs.

Scanline Padding

Another thing you have to keep in mind with bitmaps is their pixel width must always be a multiple of 4. So even if the picture is 3 pixels across, you have to add a pixel of padding on each line.

// determine padding for scanlines
int padding =  (4 - (bi.biWidth * sizeof(RGBTRIPLE)) % 4) % 4;

Copying RBG Pixels

Then we just loop over the pixels, reading them one at a time and writing them out. This particular exercise required that we take out the red pixels. So we check if it's a fully red pixel, and if it is replace it with a white pixel. The the rest of the image we change into a grayscale average to identify the person.

// iterate over infile's scanlines
for (int i = 0, biHeight = abs(bi.biHeight); i < biHeight; i++)
{
    // iterate over pixels in scanline
    for (int j = 0; j < bi.biWidth; j++)
    {
        // temporary storage
        RGBTRIPLE triple;

        // read RGB triple from infile
        fread(&triple, sizeof(RGBTRIPLE), 1, inptr);
         
        // Check if pixel is red and replace with white
        if (triple.rgbtBlue == 0x00 && triple.rgbtGreen == 0x00 && 
            triple.rgbtRed == 0xff)
        {
            triple.rgbtBlue = 0xff;
            triple.rgbtGreen = 0xff;
        }

        // Make grayscale value of the rest of the image
        else
        {
            int avg = (triple.rgbtBlue + triple.rgbtGreen + 
                       triple.rgbtRed) / 3;
            triple.rgbtBlue = avg;
            triple.rgbtGreen = avg;
            triple.rgbtRed = avg;
        }

        // write RGB triple to outfile
        fwrite(&triple, sizeof(RGBTRIPLE), 1, outptr);
    }

    // skip over padding, if any
    fseek(inptr, padding, SEEK_CUR);

    // then add it back (to demonstrate how)
    for (int k = 0; k < padding; k++)
    {
        fputc(0x00, outptr);
    }
}

Then it's just a matter of closing the files, freeing up the memory. It's fairly simple, just:

// close infile
fclose(inptr);

// close outfile
fclose(outptr);

Resizing

Once you need to resize an image, reading through those header sections becomes even more important.

// Get Original file's padding
int oldPadding =  (4 - (in_bi.biWidth * sizeof(RGBTRIPLE)) % 4) % 4;

// Update width, and height in Bitmap info header
out_bi.biWidth = floor(out_bi.biWidth * n);
out_bi.biHeight = ceil(out_bi.biHeight * n);

// determine padding for scanlines
int newPadding =  (4 - (out_bi.biWidth * sizeof(RGBTRIPLE)) % 4) % 4;
   
// Update size in Bitmap info header
out_bi.biSizeImage = out_bi.biWidth * abs(out_bi.biHeight) * 
    sizeof(RGBTRIPLE) + newPadding * abs(out_bi.biHeight);

// Update Size in Bitmap File Header
out_bf.bfSize = out_bf.bfOffBits + out_bi.biSizeImage;

Basically it's a bunch of calculations to find the new padding and size, otherwise the header info won't match the image and you won't be able to open it.

Then you just have to make sure when you're looping through the pixels to repeat each pixel n number of times both vertically and horizontally.

For the full code for each challenge, you can find it on Github. It also has another exercise of reading jpg files off of a damaged sd card, which is a pretty cool experiment, but mostly the same file operations as these.