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.
You can find the source files and image examples used here on the GitHub repo for this article.
The article consists of two parts:
Let’s start by getting the libraries from GitHub:
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:
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.
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:
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:
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:
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:
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:
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:
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.:
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:
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.
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:
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:
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: