Solarian Programmer

My programming ramblings

C Programming - Reading and writing images with the stb_image libraries

Posted on June 10, 2019 by Paul

In this article I will show you how to read and write images with the stb_image libraries. In order to exemplify the usage of the library I’ll demo how to convert an image to gray and how to apply a sepia filter to the image.

As a side note, the C code from this article is compatible with any modern C compilers like GCC, Clang and MSVC.

You can find the source files and image examples used here on the GitHub repo for this article.

The article consists of two parts:

stb_image basic usage

Let’s start by getting the libraries from GitHub:

1 git clone https://github.com/nothings/stb.git

if you don’t have git installed on your computer you can use the Download ZIP option.

Next, copy the three files prefixed with stb_image from the stb folder in a new folder named stb_image that, from now on, I will assume it is present in your project folder. Optionally, you can remove the stb folder from your machine.

There is also a video version of this part of the tutorial:

We’ll start with an example of basic usage of the library functions. We’ll read an image from the disk and write it back. The purpose of this example is to familiarize you with the library interface.

We need to define STB_IMAGE_IMPLEMENTATION before including the stb_image.h header. This needs to be defined only once. If you need to include the stb_image header in another C source file don’t redefine the STB_IMAGE_IMPLEMENTATION. Same considerations apply to defining STB_IMAGE_WRITE_IMPLEMENTATION before including the “stb_image_write” header file:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 #define STB_IMAGE_IMPLEMENTATION
 5 #include "stb_image/stb_image.h"
 6 #define STB_IMAGE_WRITE_IMPLEMENTATION
 7 #include "stb_image/stb_image_write.h"
 8 
 9 int main(void) {
10     // ...
11 }

In order to read an image from the disk we’ll use the stbi_load function that receives as arguments the image file path, width and height of the image, the number of color channels and the desired number of color channels. The last argument of the function is useful if for example you want to load only the R, G, B channels from a four channel, PNG, image and ignore the transparency channel. If you want to load the image as is, pass 0 as the last parameter of the load function. In case of error, the stbi_load function returns NULL.

 1 // ...
 2 
 3 int main(void) {
 4     int width, height, channels;
 5     unsigned char *img = stbi_load("sky.jpg", &width, &height, &channels, 0);
 6     if(img == NULL) {
 7         printf("Error in loading the image\n");
 8         exit(1);
 9     }
10     printf("Loaded image with a width of %dpx, a height of %dpx and %d channels\n", width, height, channels);
11 
12     // ...
13 }

After you’ve finished processing the image, you can write it back to the disk using one of the stbi_image_write functions. Here I’ll show you how to save the above loaded image as PNG and JPG images:

 1 // ...
 2 
 3 int main(void) {
 4     // ...
 5 
 6     stbi_write_png("sky.png", width, height, channels, img, width * channels);
 7     stbi_write_jpg("sky2.jpg", width, height, channels, img, 100);
 8 
 9     stbi_image_free(img);    
10 }

The 6th parameter of the stbi_write_png function from the above code is the image stride, which is the size in bytes of a row of the image. The last parameter of the stbi_write_jpg function is a quality parameter that goes from 1 to 100. Since JPG is a lossy image format, you can chose how much data is dropped at save time. Lower quality means smaller image size on disk and lower visual image quality. Most image editors, like GIMP, will save jpg images with a default quality parameter of 80 or 90, but the user can tune the quality parameter if required. I’ve used a quality parameter of 100 in all examples from this article.

Once you are done with an image, you can release the memory used to store its data with the stbi_image_free function.

You can try the above code by building and running t0.c from the article repo.

As mentioned before, you can load only the first channels of an image. Here is an example of loading the first three color channels of a PNG image:

 1 // ...
 2 
 3 int main(void) {
 4     int width, height, original_no_channels;
 5     int desired_no_channels = 3;
 6     unsigned char *img = stbi_load("Shapes.png", &width, &height, &original_no_channels, desired_no_channels);
 7     if(img == NULL) {
 8         printf("Error in loading the image\n");
 9         exit(1);
10     }
11     printf("Loaded image with a width of %dpx, a height of %dpx. The original image had %d channels, the loaded image has %d channels.\n", width, height, original_no_channels, desired_no_channels);
12 
13     // ...
14 }

Observation, if you decide to load only a certain number of channels from an image, be careful to use the desired number of channels in further operations on the image data. The fourth argument of the stbi_load function will be initialized with the original number of channels of the image. For example, if you want to save the above loaded image you will use something like this:

1 // ...
2 
3 int main(void) {
4     // ...
5 
6     stbi_write_png("1.png", width, height, desired_no_channels, img, width * desired_no_channels);
7 
8     // ...
9 }

You can see a complete example of partially loading an image in t01.c from this article repo.

Another observation, if you are interested only in the image general information, like the image size and number of channels, you can avoid loading the image in memory by using the stbi_info function which will initialize the width, height and number of channels parameters. Here is a snippet example:

1 // ...
2 
3 int main(void) {
4     int width, height, channels;
5     const char *fname = "Shapes.png";
6     stbi_info(fname, &width, &height, &channels);
7 
8     // ...
9 }

As a first example of image manipulation, I will show you how to convert the loaded image to gray and save it to the disk. The purpose of this example is to show you how to loop over an image data and how to access and modify the pixel values.

Let’s assume that you’ve successfully loaded a PNG or JPG image and that you want to convert this to gray. The output image will have two or one channels. For example, if the input image has a transparency channel this will be simply copied to the second channel of the gray image, while the first channel of the gray image will contain the gray pixel values. If the input image has three channels, the output image will have only one channel with the gray data.

Here is a possible implementation of setting the number of channels and allocating memory for the gray image:

 1 // ...
 2 
 3 int main(void) {
 4     // Load an image
 5     // ...
 6 
 7     // Convert the input image to gray
 8     size_t img_size = width * height * channels;
 9     int gray_channels = channels == 4 ? 2 : 1;
10     size_t gray_img_size = width * height * gray_channels;
11 
12     unsigned char *gray_img = malloc(gray_img_size);
13     if(gray_img == NULL) {
14         printf("Unable to allocate memory for the gray image.\n");
15         exit(1);
16     }
17 
18     // ...
19 }

Next, we’ll loop over the pixels of the input image, calculate the gray value as the average of the red, green and blue channels and store the gray value in the output image. If the input image has a transparency channel, we’ll copy the values of this to the second channel of the output gray image:

 1 // ...
 2 
 3 int main(void) {
 4     // Load an image
 5     // ...
 6 
 7     // Convert the input image to gray
 8     // Allocate memory for the output image
 9     // ...
10 
11     for(unsigned char *p = img, *pg = gray_img; p != img + img_size; p += channels, pg += gray_channels) {
12         *pg = (uint8_t)((*p + *(p + 1) + *(p + 2))/3.0);
13         if(channels == 4) {
14             *(pg + 1) = *(p + 3);
15         }
16     }
17 
18 	// ...    
19 }

In the above code the p pointer will go over the input image, while the pg pointer will go over the output image.

Once the gray image is filled, you can save it as before, e.g.:

 1 // ...
 2 
 3 int main(void) {
 4     // Load an image and convert it to gray
 5     // ...
 6 
 7     stbi_write_jpg("sky_gray.jpg", width, height, gray_channels, gray_img, 100);
 8 
 9 	// ...    
10 }

Next, I will show you how to convert a color image to sepia. In this case the output image will have the same size in bytes as the input image. The color channels of the sepia image are a mix of the color channels of the input image:

 1 // ...
 2 
 3 int main(void) {
 4     // Load an image
 5     // ...
 6 
 7     // Convert the input image to sepia
 8     unsigned char *sepia_img = malloc(img_size);
 9     if(sepia_img == NULL) {
10         printf("Unable to allocate memory for the sepia image.\n");
11         exit(1);
12     }
13 
14     // Sepia filter coefficients from https://stackoverflow.com/questions/1061093/how-is-a-sepia-tone-created
15     for(unsigned char *p = img, *pg = sepia_img; p != img + img_size; p += channels, pg += channels) {
16         *pg       = (uint8_t)fmin(0.393 * *p + 0.769 * *(p + 1) + 0.189 * *(p + 2), 255.0);         // red
17         *(pg + 1) = (uint8_t)fmin(0.349 * *p + 0.686 * *(p + 1) + 0.168 * *(p + 2), 255.0);         // green
18         *(pg + 2) = (uint8_t)fmin(0.272 * *p + 0.534 * *(p + 1) + 0.131 * *(p + 2), 255.0);         // blue        
19         if(channels == 4) {
20             *(pg + 3) = *(p + 3);
21         }
22     }
23 
24     stbi_write_jpg("sky_sepia.jpg", width, height, channels, sepia_img, 100);
25     
26 
27 	// ...    
28 }

You can find a complete example of loading an image and converting it to gray and sepia as t1.c in the repo for this article.

Writing a wrapper around the stb_image functions

In this part of the article we are going to abstract the code presented before to a small image library. The advantage of using a small abstraction over directly calling the stb_image functions is that we can put all image related information in a structure and write some utility functions that manipulate the Image struct. We can also reduce the possibility of user errors by presenting a simpler interface.

There is also a video version of this part of the tutorial:

We’ll start by writing the Image header file which contain the public interface for our small library:

 1 #pragma once
 2 
 3 #include <stdlib.h>
 4 #include <stdint.h>
 5 #include <stdbool.h>
 6 
 7 enum allocation_type {
 8     NO_ALLOCATION, SELF_ALLOCATED, STB_ALLOCATED
 9 };
10 
11 typedef struct {
12     int width;
13     int height;
14     int channels;
15     size_t size;
16     uint8_t *data;
17     enum allocation_type allocation_;
18 } Image;
19 
20 void Image_load(Image *img, const char *fname);
21 void Image_create(Image *img, int width, int height, int channels, bool zeroed);
22 void Image_save(const Image *img, const char *fname);
23 void Image_free(Image *img);
24 void Image_to_gray(const Image *orig, Image *gray);
25 void Image_to_sepia(const Image *orig, Image *sepia);

The Image struct from the above header file is self explanatory, you can find it as Image.h in the article repo. The last field of the Image struct, the allocation type enumeration, is used to record if the memory was allocated by the user or by one of the stb_image functions.

Next, we have utility functions to load, create, save and free an image. For simplicity, we’ve assumed that the user will save to disk only PNG or JPG images. The code can be easily extended with other output functions from the stb_image_write or with new functions written by the user.

The last two functions from Image.h are used to convert an input image to gray or sepia. We basically, take the code written in t1.c and put it in separate functions. These two functions will also allocate the necessary memory for the output image.

The actual implementation of the above functions is in Image.c. The implementation code is basically a modified version of the code from t1.c, so it won’t be presented in the article.

Here is an example of using the above library to load two input images: sky.jpg and Shapes.png, convert the images to gray and sepia, save the output images and free the memory used for storing the input and output images:

 1 #include "Image.h"
 2 #include "utils.h"
 3 
 4 int main(void) {
 5     Image img_sky, img_shapes;
 6 
 7     Image_load(&img_sky, "sky.jpg");
 8     ON_ERROR_EXIT(img_sky.data == NULL, "Error in loading the image");
 9     Image_load(&img_shapes, "Shapes.png");
10     ON_ERROR_EXIT(img_shapes.data == NULL, "Error in loading the image");
11 
12     // Convert the images to gray
13     Image img_sky_gray, img_shapes_gray;
14     Image_to_gray(&img_sky, &img_sky_gray);
15     Image_to_gray(&img_shapes, &img_shapes_gray);
16 
17     // Convert the images to sepia
18     Image img_sky_sepia, img_shapes_sepia;
19     Image_to_sepia(&img_sky, &img_sky_sepia);
20     Image_to_sepia(&img_shapes, &img_shapes_sepia);
21 
22     // Save images
23     Image_save(&img_sky_gray, "sky_gray.jpg");
24     Image_save(&img_sky_sepia, "sky_sepia.jpg");
25     Image_save(&img_shapes_gray, "Shapes_gray.png");
26     Image_save(&img_shapes_sepia, "Shapes_sepia.png");
27 
28     // Release memory
29     Image_free(&img_sky);
30     Image_free(&img_sky_gray);
31     Image_free(&img_sky_sepia);
32 
33     Image_free(&img_shapes);
34     Image_free(&img_shapes_gray);
35     Image_free(&img_shapes_sepia);
36 }

A special mention about utils.h used in the above code, this header file contains an error checking helper macro that, in case of error, will print the calling function and line number were the error was detected. You can find the above code as t2.c in the article repo.

If you want to learn more about C99/C11 I would recommend reading 21st Century C: C Tips from the New School by Ben Klemens:

or the classic C Bible, The C Programming Language by B.W. Kernighan, D.M. Ritchie:


Show Comments