Week 8

Digital Images

In this lesson we'll look at processing digital images. Here, for instance, is a picture that my wife took on vacation in Madrid, of me standing in front of the statue of Don Quixote and Sancho Panza. Stephen Gilbert in Madrid with image enlarged.

Zooming in on my face, we can see that the image is actually made up of many square "pixels", each showing one color.

  • Each pixel is a small square that shows a single color
  • An 800 x 600 image is 800 pixels wide by 600 pixels high, which is 480,000 pixels in all (0.5 megapixels).
  • The digital camera in your phone produces images with several megapixels per image. For instance, a 8000 x 6000 pixel image would be about 5 megapixels. The iPhone 13 camera is 12 megapixels

Every pixel has an x,y coordinate that identifies its location within the image grid.

Week 8

RGB Colors

If every pixel is a single color, then how do we represent that color in memory? We use a scheme called RGB, or red, green blue. By combining different quantities of pure red, green and blue light, we can create any color at all.

Each of the red, green and blue light levels is encoded as a number in the range 0..255, with 0 meaning zero light and 255 meaning maximum light.

So for example (red=255, green=100, blue=0) is a color where red is maximum, green is medium, and blue is not present at all, resulting in a shade of orange. In this way, specifying the brightness 0..255 for the red, blue, and green color components of the pixel, any color can be formed.

Pigment Note -- you may have mixed color paints, such as adding red and green paint together. That sort of "pigment" color mixing works totally differently from the "light" mixing we have here. Light mixing is easier to follow, and in any case, is the most common way that computers store and manipulate images.

It's not required that you have an intuition about, say, what blue=137 looks like. You just need to understand that any color can be made by combining the three color values.

Week 8

The RGB Explorer

Try it here!

This pages lets you play with the RGB scheme, combining red, green, and blue light to make any color. The sliders control the red green and blue lights, each ranging from 0 (off) to 255 (maximum). The intersecting rectangles show the result of adding the red, green, and blue light together—any color can be created in this way.

To make pure red, green, or blue light, just turn up that color, leaving the other two at 0. A few other common combinations:

  • All at max (255) → white
  • All at min (0) → black
  • red + green → yellow
  • red + blue → purple
  • green + blue → turquoise
  • Dark yellow—make yellow, then reduce both red and green
  • Orange—make yellow, but more red, less green
  • Light, pastel green—make pure green, then turn up both red and blue some equally (going towards white)
  • Light gray—make white, then turn all three down a bit equally
Week 8

Digital Images in C++

Icon for links to Replit project.

C++ itself doesn't have any built-in support for images, graphics or user-interfaces. All of those capabilities are added using libraries. Click on the little Running-Man in the right to open the Replit project for this lesson. Make sure you Fork the Repl to get your own copy to work on.

We're going to use the stb_image and stb_image_write libraries, written by Sean T. Barret (stb) and placed into the public domain. These libraries provide the ability to read and write several different image formats, in several different ways. Both are C libraries instead of C++ libraries.

The STB libraries are header only libraries. That means you only need to include the header file; there is no separate library to compile and link to. I've already added the header files to the Repl you are working with in this lesson.

In your own programs, https://github.com/nothings/stb download the latest version of the stb_image.h and stb_image_write.h from the GitHub site

and place both files in your project folder. To make sure that the implementation is included, in one file, you need the following preprocessor directives before you include them:
#define STB_IMAGE_IMPLEMENTATION        // REQUIRED (loading)
#define STB_IMAGE_WRITE_IMPLEMENTATION  // writing
#include "stb_image.h"                  // "header-only" C libraries
#include "stb_image_write.h"

I've already added these to main.cpp in the Repl you are working with.

Week 8

Loading an Image

When a digital image is stored on disk, it is compressed and encoded using a specific encoding scheme, such as JPEG, PNG, or GIF. The stb library function, stbi_load() will:

  • Open the file and read and decode the image data
  • Allocate enough memory to store the pixels on the heap
  • Return a pointer to the image data as well as the width, height and bytes-per-pixel used to encode the original image.
Week 8

More Loading an Image

The first part of the sample program (in main.cpp), loads a JPEG version of OCC's mascot, Pete the Pirate, into memory.

int width, height, bpp, channels = 4;
unsigned char * const pete =
    stbi_load("pete.jpg",             // input file
              &width, &height, &bpp,  // pointers (out)
              channels);              // channels (in)
  • The stbi_load() function returns a pointer to the first byte of the image data in memory. The type of the pointer is an unsigned char, which, in C++ speak means a "raw" byte. If loading fails, then the function returns the nullptr.

    Note that the pointer pete is a const pointer. This is necessary because you will later need to "free" the memory that the function has placed on the heap. If you move the pointer, then your program may crash.

  • The first argument to the function is the path to the file. This can be absolute or relative (as used here), but it must be a C-style string. We'll look more at C-style strings in a future lesson. For right now, use a literal or use the c_str() member function on a regular C++ string.
  • The next three arguments are the address of the width, height, and bytes-per-pixel used in the original image. These are output parameters; that means that you first create the variables (on line 1), and then pass their addresses as arguments. The function will fill them in. The information flows out of the function, not into it.
  • The last argument is an input parameter telling the stbi_load() function how to store the image. Here we're telling it to store 4 bytes for each pixel (RGBA), even though the original image only has 3 (RBG).
Week 8

Saving an Image

When we save the image, we can use a different format from the original. For instance, JPEG doesn't have transparent colors, but we can write the image back out as a PNG, which does. The stb_image_write library has different functions for each image type. Here's the code to save our image as PNG.

stbi_write_png("pete.png", width, height, channels, pete,
        width * channels);

Each file type you want to use has its own function, but the first five arguments are the same for each file type:

  1. The file name as a C-style string. Here we've hard-coded pete.png
  2. The width and height returned from stbi_load().
  3. The number of channels (or bytes-per-pixel) used in memory to represent the image.
  4. The pointer to the first byte of the image data in memory.

The last argument, width * channels, is unique to PNG files. It tells the function at what byte the next row begins.

Freeing the Memory

The stbi_load() function returns a pointer, but inside that function it asks the operating system to allocate enough memory to hold the image that it loads from disk. This memory is on the heap, which you met in an earlier lesson. In the C programming language, you have to remember to free that memory before your program ends. We do that by using the function stbi_image_free(pete).

Week 8

Changing the Format

A 4-channel stegosaurus image.

Although the previous example added an alpha (transparency) channel to the Pete the Pirate picture, it didn't really change how it looked. Our second example loads this 4-channel PNG image as a 1-channel (that is, grayscale) image. We'll then save it as a BMP which is the native format for Windows applications.

Here's the code that that does this.

//Load a png file using 1 byte per pixel (Gray scale)
channels = 1;
unsigned char * const stego = 
    stbi_load("stegosaurus.png", &width, &height, &bpp, channels);
stbi_write_bmp("stego-bw.bmp", width, height, channels, stego);
stbi_image_free(stego);
A 1-channel stegosaurus image.

Notice that the conversion from 4-channel image on disk to 1-channel image in memory, happened when we loaded the image, not when we wrote the image.

PNG to JPEG

The first example changed a JPEG into a PNG, but the final example goes the other way, removing the alpha (transparency) channel and saving it as a JPEG. The stb_write_jpg() function also takes one extra argument, which is the quality of the resulting image. This can go from 0-100, where 100 has the highest quality.

Go ahead now and type make run in the Repl Shell, and you'll be able to examine the modified images. In your homework, we'll use this newfound ability to read and write images to write image filters, like those found in PhotoShop.