The lolcat image above has Edgar Allan Poe’s poem, The Raven, hidden inside the color values of the pixels. The rest of this post will describe how that was done.
##Prerequisite - Understanding How Images are Stored as Pixels
Writing image filters with students is an excellent way to discuss how images are stored in memory, and provide a nice example of using nested for loops and arrays. When I show my students basic image filtering in Processing, I loosely follow Daniel Shiffman’s examples from his Learning Processing book. The general order goes something like:
- set the pixels of the entire canvas to random colors - Example 15-5
- make a vertically striped image, by turning every even pixel white, and every odd pixel black - Example 15-6
- demonstrate how to examine the pixel array of a specific image - Example 15-7
- create a flashlight-like image filter (set pixels black if they are a certain distance away from the mouse location) - Example 15-9
- create a threshold image filter (if the brightness of a pixel is sufficiently high, set it to white, else set it to black) - Example 15-10
- students create their own filters, then show them to each other
Now that the students have a grasp of how image pixels are stored as an array, as well as how to access and change the RGB values of each pixel, you are all set for hiding messages! The basic algorithm I’m going to use is:
- set all red component values of pixels to even numbers, by subtracting by 1 if necessary. This is simply removing the least significant bit from the red color component (you could, of course, do this with green and blue as well).
- convert the message to be hidden into binary
- loop through the pixels of the image, continuing until message bits have been applied, by:
- leaving red component as is (if the bit is a 0)
- adding 1 to the red component (if the bit is a 1)
Since we can convert any english character into an ASCII value, we will use up 8 bits per character when encoding. We can therefore hide one character in every 8 pixels of the image (if you were to include the green and blue color components as well, you could put 3 characters into every 8 pixels).
##Set All Red Components of Pixel Color Values to Even Numbers
Since each color component (RGB) is an integer from 0 to 255, a person will not notice the difference in the image if we change some of the pixels red component values by 1. Knowing this, we can set all of the red values to even numbers to establish a baseline to work from. This is as simple as checking if
someNum % 2 == 0. If there is no remainder when dividing by two, it is already even, so we do not change anything. If there is a remainder, we subtract the red value by one.
##Converting Text to Binary
Before converting the secret message into binary, we should confirm that it will actually fit inside the image. Since each character of the message will require 8 pixels of storage, we need to confirm that
(message.length() * 8) < (theImage.width * theImage.height). Assuming that this check is passed, Processing has a built-in binary function that gives us an easy way to convert any character into it’s binary representation. We can just loop through the secret message one character at a time, converting each of the characters into binary as we go. This will produce a large string filled with 1’s and 0’s, representing the secret message.
##Applying the Message to the Image
To save the message (now stored as that string of 0’s and 1’s) into the image, all we need to do is loop through the string. Every time the current character is a 0, just leave the related image pixel as is (an even number). If the current character is an odd number, however, we should add one to the red value (to make the red component an odd number, representing a binary 1).
##Decoding the Image
Decoding is just the opposite of what we did above. Loop through the pixels in an image, checking each to see if it has an odd or even red component value. If it is even, save a 0 into the binary string representing the decoded message. If it is odd, save a 1 instead. Once the entire image has been converted into binary (you can just stop after encountering a bunch of 0s in a row), loop through the binary string, examining 8 binary values at a time and converting those 8 values back into an ASCII character. In Processing, this can be done by creating a string containing the 8 binary values,
String currentChar = encodedMessage.substring(i, i+8);, then appending the ASCII value of that to the decoded message,
decodedMessage = decodedMessage + char(unbinary(currentChar)); .