Basic Imageprocessing in C Part 1
Introduction
In this series of blog posts I will implement basic filters with kernels for imageprocessing all from scratch in C. In order to be able to perform imageprocessing operations on an Image we first need to read an Image and also save its pixel values in a buffer. Most images that you see online are either in jpeg format or in png format. Since I do not want to get caught up dealing with these more complicated formats, I am going to restrict myself to using images in uncompressed PPM format for now (although we could also use the famous stb library header for C). Keep in mind that the PPM format is very inefficient and super bloated kinda like a furry fetish, which is why you rarely use them in actual programs. You can read more about the PPM format and its siblings [1].
How do I get a PPM Image
You can take an image in PNG format from the internet and convert it into a PPM quite easily by using the CLI of Imagemagick assuming you have Imagemagick installed. You could also get a PPM Image directly from the internet although they are hard and difficult to find, so I recommend to do the former instead.
convert someimage.png -compress none someimage.ppm
You should now have a human readable PPM file. You can display the first few lines of that PPM image using the following command.
cat someimage.ppm | head
The top of the file should look vaguely similar to what I have.
P3
512 512
255
226 137 125 226 137 125 223 137 133 223 136 128 226 138 120
226 129 116 228 138 123 227 134 124 227 140 127 225 136 119
228 135 126 225 134 121 223 130 108 226 139 119 223 135 120
221 129 114 221 134 108 221 131 113 222 138 121 222 139 114
223 127 109 223 132 105 224 129 102 221 134 109 218 131 110
221 133 113 223 130 108 225 125 98 221 130 121 221 129 111
220 127 121 223 131 109 225 127 103 223 134 109 226 128 106
The first three lines of the PPM file above are whats known as the header information of that file. The “P3” at the very beginning of our file is the signature of the ASCII PPM format sometimes also referred to as the “magic bytes” or “magic number” of a file. Without the presence of a signature the encoder or program wouldn’t know what kind of file they are dealing with. The next line has two integers. These are simply the dimensions of our image. In my case I have a 512 by 512 image. The third line has a single integer number. This number is the maximum color value that each color channel can have. Since I had a 24-bit image the maximum color value is 255, if the maximum color value is lower than 255 we would have an image with a lower color resolution.
The next integers outside of the file header represent the actual color information of our image. The first integer (226) encodes the color value of the red channel, the second integer (137) encodes the color value of the green channel and the third integer encodes the color value of the blue channel (125). These three integers encode the color of a single pixel in our image. Since our image is 512x512 and we need three integers to represent a pixel that means we have a total of 786432 integers that are outside of the file header. You can imagine how inefficient this format is for images especially for bigger ones.
Implementation
Now that we have talked about the PPM format we can program some structures in C that will act like buffers so that we can perform various different image processing operations without the need to constantly read from the file. First lets create a C structure for the information we find in our header of the PPM file.
typedef struct {
uint16_t width, height;
int depth;
} ImageHeader;
The width and height are unsigned 16 bit integers and the depth describes our maximum color value. Now lets create two C structures to for the actual color information stored in the PPM file.
typedef struct {
float red;
float green;
float blue;
} Color;
typedef struct {
size_t rows, cols;
Color *matrix;
} ImageData;
Since every pixel’s color is represented by three values we create the Color structure, where the value of each color channel is a float. We also could have used an unsigned 8-bit integer. Our ImageData structure stores the number of rows and cols, this is essential because for every operation we need to iterate through the buffer. Then we store the actual color information as a pointer to a Color. We use a single pointer here, because if we used double pointers, we would have to make multiple memory allocations, one allocation for every row in the buffer, and with just a single pointer we only have to allocate memory once.
Now lets create a function that will construct a structure and return a pointer to that structure. This is somewhat similar to how a constructor in an object oriented language works.
ImageData *createImageBuffer(FILE *sourceImage, int rows, int cols,
int paddingOffset) {
ImageData *data = (ImageData *)malloc(sizeof(ImageData));
data->rows = rows + 2 * paddingOffset;
data->cols = cols + 2 * paddingOffset;
data->matrix = (Color *)calloc(data->rows * data->cols, sizeof(Color));
int i, j, red, green, blue;
for (i = paddingOffset; i < data->rows - paddingOffset; i++) {
for (j = paddingOffset; j < data->cols - paddingOffset; j++) {
fscanf(sourceImage, "%f %f %f", &data->matrix[i * data->cols + j].red,
&data->matrix[i * data->cols + j].green,
&data->matrix[i * data->cols + j].blue);
}
}
return data;
}
This function creates an ImageData structure and also returns a pointer to that resource, so that we can pass a pointer to that buffer to different functions that change the data in the buffer. In the parameter list of that function we expect a FILE pointer, pointing to the actual PPM file, a rows and a cols number specifying the dimensions of our image and also a paddingOffset. The paddingOffset is going to be important for the image processing techniques that require a kernel of which there are many.
In C it is advisable to have another function that destructs that buffer to avoid memory leaks.
void destructBuffer(ImageData *data) {
if (data != NULL) {
free(data->matrix);
free(data);
}
}
We also have a function that makes the ImageHeader structure.
ImageHeader writeImageHeader(FILE *sourceImage, FILE *outputImage) {
ImageHeader header = {0};
uint16_t w, h;
int depth;
char str[5];
fscanf(sourceImage, "%s %d %d %d", str, &w, &h, &depth);
fprintf(outputImage, "%s\n%d %d\n%d\n", str, w, h, depth);
header.width = w;
header.height = h;
header.depth = depth;
return header;
}
The function returns an ImageHeader and writes directly into the file we want to output the header information we got from the source image. We do this because the header information of both the source image and the output image stays the same for now. The width and the height fields of the ImageHeader will be useful once we make a function call to the createImageBuffer function as we need those for the parameter list.
After we performed all of the image processing operations we want to perform on our ImageData buffer, we need to be able to write the contents of the mutated buffer into a PPM file. We do this via a new function.
void readBufferToOutput(ImageData *data, int paddingOffset, FILE *outputImage) {
int i, j;
for (i = paddingOffset; i < data->rows - paddingOffset; i++) {
for (j = paddingOffset; j < data->cols - paddingOffset; j++) {
fprintf(outputImage, "%d %d %d \n",
(int)data->matrix[i * data->cols + j].red,
(int)data->matrix[i * data->cols + j].green,
(int)data->matrix[i * data->cols + j].blue);
}
}
}
This function writes the buffer into the output image and ignores the padding since we don’t need the padding in our output image.
Closing Remarks
So now we basically got all the functionality to
- Read color data from a PPM file
- Read header data from a PPM file
- Write the ImageData buffer into a PPM file
This will give us a good basis for the implementation of a simple image processor. In part 2 of this series I will probably talk about grayscale.
References
[1] PPM Format Specification https://netpbm.sourceforge.net/doc/ppm.html)