cimage-processingbinaryppmpixelate

How to pixelate a binary (P6) PPM file


I am struggling to pixelate an image which is made up of RGB values stored in a binary (P6) PPM file. The steps to pixelate the image are as follows:

  1. Read in the binary data and store it in a 1-dimensional array
  2. Iterate through the data stored in this array, in cells of size 'c' (row x columns). This variable 'c' can be changed by the user, but for this program it's currently set to int c = 4; meaning it iterates through pixel data in block dimensions of 4 x 4
  3. Now find the average colour value within each of those cells of size 'c'
  4. Output each average colour value to a new output PPM file

Each binary PPM file begins with a header in the following format, consisting of a 'magic number', followed by the width, height, and finally maximum colour value of 255. Header comments are ignored. The following header example shows a PPM image of format P6 (therefore a binary file), a width of 16, a height of 16, and a max colour value of 255:

P6
16
16
255 

Where I am struggling:

  1. I am unsure of how to find the average RGB value for each cell, and then output it to the new data stream. I figured out a way to do this when reading the data linearly (i.e. not reading it into an array (or buffer)) by dividing the total colours in that cell by the number of loop iterations, but this proved to be incorrect.
  2. Are the nested for loops ordered in a way which changes the orientation of the output image e.g. is it rotating it 180 degrees etc? I am struggling to determine this with reading raw binary data.

My attempt:

#define _CRT_SECURE_NO_WARNINGS                 //preprocessor requirement 

#include <stdio.h>                              //library for I/O functions 
#include <stdlib.h>                             //library for general functions 

#define magic_size 10                           //macro for PPM character found within header

typedef struct {
    int t_r, t_g, t_b;                          //Struct to hold  RGB pixel data
} Pixel;

int main()
{
    char magic_number[magic_size];              //variable for PPM format
    int width = 0, height = 0, max_col = 0;     //imagine dimensions
    int c = 4;                                  //mosaic parameter

    /* INPUT FILE HANDLING */
    FILE *inputFile; 
    inputFile = fopen("Sheffield512x512.ppm", "r");
    //input file error handling
    if (inputFile == NULL)
    {
        printf(stderr, "ERROR: file cannot be opened");
        getchar();  //prevent cmd premature closure
        exit(1);    //exit program cleanly
    }

    /* OUTPUT FILE HANDLING */
    FILE *outputFile; 
    outputFile = fopen("mosaic.ppm", "w");
    //output file error handling
    if (outputFile == NULL)
    {
        printf(stderr, "ERROR: cannot write to file");
        getchar();  //prevent cmd premature closure
        exit(1);    //exit program cleanly
    }

    // Scan the header (these variables are used later on)
    fscanf(inputFile, "%s\n%d\n%d\n%d", &magic_number, &width, &height, &max_col);

    // Error handling. Program only supports binary files (i.e. of P6 format) 
    if (magic_number[1] != '6')
    {
        printf("Only Binary images supported!\n");
        getchar();  //prevent cmd premature closure
        return;
    }

    // Raw 1 dimensional store of pixel data
    Pixel *data = malloc(width*height * sizeof(Pixel));

    //2D index to access pixel data
    Pixel **pixels = malloc(height * sizeof(Pixel*));

    // Read the binary file data 
    size_t r = fread(data, width*height, sizeof(unsigned char), inputFile);

    // Build a 1-dimensional index for the binary data 
    for (unsigned int i = 0; i < height; ++i)
    {
        pixels[i] = data + (i * width); 
    }

    // Close the input file 
    fclose(inputFile);


    /* BEGIN PIXELATION PROCESS */

    // Print the OUTPUT file header 
    fprintf(outputFile, "%s\n%d\n%d\n%d", magic_number, width, height, max_col);

    //loop condition variables 
    int cw_x = ceil((double)(width / (float)c));
    int cw_y = ceil((double)(height / (float)c));

    //iterate through 2d array in cells of size c 
    for (int c_x = 0; c_x < cw_x; c_x += 1)
    {
        for (int c_y = 0; c_y < cw_y; c_y += 1)
        {

            //iterate within the cells
            for (int _x = 0; _x < c; _x++)
            {
                int x = c_x * c + _x;

                //bounds checking
                if (x < width)
                {
                    for (int _y = 0; _y < c; _y++)
                    {
                        int y = c_y * c + _y;

                        //bounds checking 
                        if (y < height)
                        {
                            //write data to the output FILE stream 
                            fwrite(data, width*height, sizeof(unsigned char), outputFile);
                        }
                    }
                }

            }
        }
    }

    //close the output file
    fclose(outputFile);

    return 0; 
}

Solution

  • In the comments I gave you some feedback about errors in your code. You can fix those yourself. Take a debugger to test/check all those preliminary steps. For example read the file and write it immediately (and display the image) so you know the reading is OK.

    Your main problem and question is about the loop.

    In essence you have a one-dimensional array that consists of scanline-after-scanline and each scanline contains pixels. Contrary to the BMP format, your format seems not to be using padding bytes to align scanlines on word boundaries. That makes it a bit easier.

    A pixel consists of three color values, R, G and B, and I assume each color value is one byte (unsigned char). The memory allocation and reading then becomes:

    unsigned char *data = malloc(width*height*3);
    r = fread(data, width*height*3, 1, inputFile);
    

    The loop now goes through all the lines in increments of four and processes each pixel in increments of four. So it processes one square at a time, calculates the average and writes it out:

        c= 4;
        for (y=0; y<height; y += c)
        {
            for (x=0; x<width; x += c)
            {
                unsigned int avgR=0, avgG=0, avgB= 0;
    
                for (dy=0; dy<c && y+dy<height; dy++)
                {
                    for (dx=0; dx<c && x+dx<width; dx++)
                    {
                        avgR += data[  y*width*3    // line in image
                                     + x*3          // pixel on line
                                     + dy*width*3   // line of square
                                     + dx*3         // R pixel in line of square
                                     ];
                        avgG += data[  y*width*3    // line in image
                                     + x*3          // pixel on line
                                     + dy*width*3   // line of square
                                     + dx*3 + 1     // G pixel in line of square
                                     ];
                        avgB += data[  y*width*3    // line in image
                                     + x*3          // pixel on line
                                     + dy*width*3   // line of square
                                     + dx*3 + 2     // B pixel in line of square
                                     ];
                    }
                }
                unsigned char avgRb= avgR/(dx*dy);
                unsigned char avgGb= avgG/(dx*dy);
                unsigned char avgBb= avgB/(dx*dy);
                fwrite(&avgR,1,1,outputFile);
                fwrite(&avgG,1,1,outputFile);
                fwrite(&avgB,1,1,outputFile);
            }
        }
    

    This could be optimized using pointer arithmetic, but this shows the basics of the loop you require.

    Notes:

    Disclaimer

    I couldn't test it so the algorithm is a mental construction.