This is the last post of the series about image manipulation for C/C++ beginners. We will see how to create a simple filter to process an image using C. The filter we will implement is named 'embossing filter'. We can find this type of filter image in manipulation programs and image packages like GIMP, Photoshop and ImageMagick.
The embossing filter implementation
We will write a function that takes a pointer to an image as an argument and returns another pointer to a filtered image. As a start, the function calculates the difference between each pixel channel's value and the value of the channels of its upper left neighbour. Then saves the highest absolute difference, adding to it 128. If there are multiple equal differences with different signs (e.g. -3 and 3), the function will favour the red channel's difference first, then the greens, then the blues. A value above 255 and below 0 will be set to 255 or 0, respectively.
The following code is the C implementation of the embossing filter:
Image * emboss_image(Image * img)
{
//get size of image
unsigned int w = getImgWidth(img);
unsigned int h = getImgHeight(img);
//create a new image to return
Image * emb_img = new_image(w, h);
Pixel diff;
Pixel upleft;
Pixel curr;
char maxDiff, tmp = 0;
unsigned char v;
//for each pixel
int x, y;
for(y=1; y<h; y++)
{
for(x=1; x<w; x++)
{
if(maxDiff > tmp)
tmp = maxDiff;
else
maxDiff = tmp;
//get upper-left pixel value
upleft = getPixel(img, x-1,y-1);
//get current pixel value
curr = getPixel(img, x, y);
//find difference between channels
diff.r = curr.r - upleft.r;
diff.g = curr.g - upleft.g;
diff.b = curr.b - upleft.b;
//for equal values choose in favor of red
if((abs(diff.r)==diff.g && diff.g > diff.b)||(abs(diff.r)==diff.b && diff.b > diff.g))
maxDiff = diff.r;
//or find the maximum value
else
maxDiff = max(diff.r,max(diff.g,diff.b));
//add 128 to the maximum difference
v = 128 + maxDiff;
//remove values above 255 or below 0
if(v<0)v=0;
if(v>255)v=255;
//create and set the pixel in emb_img
Pixel val2 = {v,v,v};
setPixel(x,y,&emb_img,val2);
}
}
return emb_img;
}
Loading a PPM image in memory
Before we can process an image, we require the capacity to load and retrieve this from our computer's memory. Luckily for us, achieving this with a PPM image, it's simple. Simple as reading a text file. The following C function reads a PPM image's data and stores it inside an Image structure. Then, returns a pointer to this structure:
Image * read_PPM(const char *filename)
{
char buff[16];
Image * img;
int ch;
int rgb;
//open and read image file
FILE * fp = fopen(filename, "rb");
if (!fp) {
fprintf(stderr, "Unable to open file '%s'\n", filename);
exit(1);
}
//read image format
if (!fgets(buff, sizeof(buff), fp)) {
perror(filename);
fclose(fp);
exit(1);
}
//parse magic number
if (buff[0] != 'P' || buff[1] != '6') {
fprintf(stderr, "Invalid image format (must be 'P6')\n");
fclose(fp);
exit(1);
}
//parse comment
ch = getc(fp);
while (ch == '#') {
while (getc(fp) != '\n') ;
ch = getc(fp);
}
ungetc(ch, fp);
int width, height;
//parse width and height
if (fscanf(fp, "%d %d", & width, & height) != 2) {
fprintf(stderr, "Invalid image size (error loading '%s')\n", filename);
exit(1);
}
//parse max rgb
if (fscanf(fp, "%d", &rgb) != 1) {
fprintf(stderr, "Invalid rgb component (error loading '%s')\n", filename);
fclose(fp);
exit(1);
}
//check max rgb value correctness
if (rgb != RGB) {
fprintf(stderr, "'%s' is not a 24-bits image.\n", filename);
fclose(fp);
exit(1);
}
//create new image
img = new_image(width, height);
int bytes;
//read image data from file and store it inside pixel array
if ((bytes=fread(img->data, 3 * img->width, img->height, fp)) != img->height) {
fprintf(stderr, "Error loading image '%s'\n", filename);
exit(1);
}
fclose(fp);
return img;
}
By reading the comment in the code, it should be easy to understand what is happening. We start by opening the PPM image which we want to process. Once we did that, we need to parse the header, which will provide us with all the information's we need to process the image, as the size and the maximum value for the intensity of the colour. Also, we check for comments, as they can be present in a PPM image file, after the magic number. After we finished reading the header, we will create a new image and will store in it the data from the file. At this point, the file can be closed and we can return a pointer to our Image structure.
If you have read the emboss function, you may have noticed that we call a function called getPixel. With this function we retrieve the RGB value of each pixel stored into the Image structure pointed to by the Image pointer argument :
Pixel getPixel(Image * i, int x, int y){
return i->data[getImgWidth(i)*y + x];
}
As you can see, the function returns an element of the pixel array of the image passed as an argument. The second and the third parameters represent the coordinates of the pixel in the image plane. If the index used in the array looks weird for you, just remember that if we store the pixels of an image in a 1d array, by using the x y image plane pixel's coordinate, we can access a pixel in the 1d array by indexing this with the integer resulting from the following calculation:
pixel position = image_width * y + x
from this page you can copy or download the complete code for this post. Below you can see the image before and after applying the emboss filter:
before | after |
Conclusions
The image formats implemented in the Netpbm package, and in particular, the PPM format, represent a great and easy media to work with, especially for beginners who wish to start their journey to learning the art of computer graphics, to whom this post series was dedicated.