Digital Filters
Last week you learned how to load and save digital images, using the functions in the stb image libraries. Now, let's make some changes to those images before you save them. Programs that do this are called image filters. You may have seen them in programs like Photoshop, or in the camera app on your phone.
Although the programs we'll write won't be as sophisticated or as fast as the commercial filters you can purchase, it will give you an idea about how such programs are written. (Plus, you'll get some exercise using pointers!)
Click the "running-man" to open the starter files in ReplIt. You'll have to Fork the Repl to get your own copy. When you open main.cpp you'll see some code similar to that you used last week. This time, we're going to load our picture of "Pete the Pirate" and modify it.
Address Arithmetic
When a pointer points to a contiguous list of data elements, such as the data stored on the heap by calling stbi_load(), we can apply the operators + and – to the pointer. This is called pointer or address arithmetic. Pointer arithmetic is similar to mixed type arithmetic with integers and floating-point numbers. If you add an integer and a floating-point number, the result is a floating-point number. Similarly:
- Adding an integer to a pointer gives us a new address value.
- Subtracting one pointer from another produces an integer.
Pointer addition considers the size of the base type; it doesn't just change the address by x number of bytes. Consider this code:
vector<int> v{1, 2, 3, 4, 5};
auto *p = &v[1];
cout << "p->" << p << ", " << *p << endl;
cout << "(p+1)->" << (p+1) << ", " << *(p+1) << endl;
When run, (click the previous link) you'll see something like this:
(p+1)->0x559a1c997eb8, 3
The pointer p contains the address 0x559a1c997eb4 (although it may be a different address when you run it), and it points to the second element in the vector<int> v. The address (p + 1) is 0x559a1c997eb8. Note that for each unit that is added to a pointer value, the internal numeric value must be increased by the size of the base type of the pointer. In this case, that is 4 bytes, since the sizeof(int) is 4 on this platform.
Pointer Difference
Subtracting one pointer from another returns a signed number (of type ptr_diff) which represents the number of elements (not the bytes) between the two pointers. This is called pointer difference, and we'll use it more when we start looking at arrays.
Iterator Loops
Turning back to our image processing code, you can see that the pointer pete points to the first byte in our image once it is loaded into memory. Of course, pete is a const pointer, so it can't be changed. To process the image we need to create a pair of pointers like this:
- beg will be a non-const pointer which will move through all of the pixel data (using address arithmetic), so we can modify the image.
- end will be a const pointer that will contain the address just past the end of the data that stbi_load() has placed in memory. We can calculate this address also by using address arithmetic.
Here's the code you should add to main.cpp to create these two pointers.
unsigned char *beg = pete; // beginning of the image
unsigned char * const end = pete + width * height * channels;
Notice that the expression width * height * channels is the total number of bytes in the image. By adding it to the pointer pete, we get a new address that is pointing at the first byte following the image in memory.
With these pointers, we can "visit" every byte in the image by using this iterator loop:
while (beg != end)
{
// process the byte here
beg++; // move to the next byte
}
The Darken Filter
The first filter we'll write is "darken". Here's the algorithm:
For every byte if it is greater than 64 then subtract 64 from it rewrite the byte
Here's what the processing code looks like:
while (beg != end)
{
if (*beg > 64){
*beg = *beg - 64;
}
beg++;
}
In the Repl, you can click on the two images to see the effect of applying the filter:
Dealing with Channels
Each individual pixel in our image consist of 3 bytes, each representing an individual red, green, or blue channel in that image. So, if you want to only modify one color, or two colors, you have to keep track of which byte you are working on, by processing all three bytes every time through the loop.
Here, for instance is a filter that only keeps the blue channel, and eliminates the red and green channels in the image:
while (beg != end)
{
*beg = 0; // turn off red
beg++; // go to next color
*beg = 0; // turn off green
beg++; // go to next color
beg++; // leave blue alone
}
Here's the result of running the blue filter:
State Filters
The darken and blue filters were both process filters: they applied the same rule to all of the pixels that they encountered. A state filter is one that looks for changes in the state of a pixel, such as it's location.
If we want to keep track of where a pixel is located in the image, you need to keep track of its position and perform an action when the state changes. Here's a filter that puts a white stripe on all of the even-numbered columns in a picture.
int x = 0;
while (beg != end)
{
x++; // go to the next column
if (x % 2) { // on odd columns
*beg = 255; // turn on red
beg++; // go to next color
*beg = 255; // turn on green
beg++; // go to next color
*beg = 255; // turn on blue
beg++;
}
else { beg += 3; } // don't do anything
}
Here's what the vertical-stripes filter looks like:
Pointers & Structures
We often use pointers in conjunction with structures or objects. Pointers are also used to work with the built-in C++ collection type, the array. We'll look at structures in the lesson, and arrays later.
Click the "running man" to visualize these statements, which create two variables.
Point pt{3, 4};
Point *pp = &pt;
The variable pt is a Point with the coordinates 3 and 4. The variable pp is a pointer, pointing to pt. The memory diagram of these declarations looks like this. From pp, you move to the object by using dereferencing, so *pp and pt are synonyms.
If pt and *pp are effectively synonyms, you might expect to access pt.x by writing *pp.x. Surprisingly, you cannot. The expression *pp.x uses two operators so when you evaluate it, the dot operator has higher precedence than the dereferencing operator, so the compiler interprets the expression as *(pp.x).
Of course, pp is a pointer, and that pointer doesn't have a member called x, so you get an error. Instead, you must write (*pp).x which is certainly awkward.
A (preferred) alternative, the operator -> (usually read aloud as arrow), combines dereferencing and selection into a single operator. Knowing that, you can see there are three ways to print x and y in the variable pt:
cout << "(" << pt.x << "," << pt.y << ")" << endl;
cout << "(" << (*pp).x << "," << (*pp).y << ")" << endl;
cout << "(" << pp->x << "," << pp->y << ")" << endl;
- Line 1 uses a structure variable (an object) and the member selection operation (the "dot") operator, to select the members x and y.
- Line 2 uses the temporary structure object obtained from dereferencing the pointer pp. That object is used with the member selection operator to select the same two variables, x and y.
- Line 3 uses the pointer pp and the arrow operator to access the data members without first making a temporary copy.
Using the arrow operator is more efficient, and less typing, so you should use it when working with pointers to structures.
Using reinterpret_cast
The function stbi_load() always returns a pointer to the first byte of the digital image in memory, not the first pixel. That makes some code more complex than it needs to be. However, if you like, you can process your images pixel-by-pixel instead of byte-by-byte by following these instructions:
- Create a Pixel structure type with 3 unsigned char data members (since our image only uses 3 channels).
- When creating your beg pointer, change it to a Pixel*, not an unsigned char *, and then use reinterpret_cast<Pixel*> to cause beg to look at your image data pixel-by-pixel.
- When creating your end pointer, use beg as the base pointer. You no longer need to use channels as part of the calculation.
Here's a horizontal-stripes filter that does this, using nested for loops instead of an iterator loop, to change all the pixels in each odd numbered row to white stripe.
struct Pixel &lbracel; unsigned char red, green, blue; };
Pixel * beg = reinterpret_cast<Pixel*>(pete);
Pixel * end = beg + width * height;
for (int y = 0; y < height; ++y)
&lbracel;
for (int x = 0; x < width; ++x, ++beg)
&lbracel;
if (y % 2 == 1) &lbracel; // odd rows
*beg = Pixel&lbracel;255, 255, 255};
}
}
}
Here's what the horizontal-stripes filter looks like when it runs: