Second Title: Hiding Secret Messages in Cute Pictures of Dogs
I developed this activity to as an interesting way to introduce students taking the foundation level software class at Olin to image manipulation and binary math. Since I spent quite a bit of time working on it, I wanted to publish the entire thing as a blog post in case other people on the web are interested in steganography. This exercise was modified from a similar one found at Interactive Python, though this version encodes an image into another image instead of ASCII text.
Because I wrote this as a learning exercise, there’s a neat little repo that holds starter code and solution code. You can clone/fork it here. The starter code is in
This code uses the Python Pillow library. You can install it using pip if the package is missing from your computer by typing sudo pip install pillow into your terminal window.
If you don’t wanna learn about how to do steganography and just want a neat script that will hide messages in images for you, you can use the code in
What is steganography?
In a nutshell, the main goal of steganography is to hide information within data that doesn’t appear to be secret at a glance. For example, this sentence:
if you take the first letter of every word. Steganography is really handy to use, because people won’t even suspect that they’re looking at a secret message–making it less likely that they’ll want to try to crack your code. In the past, you may have tried to accomplish this kind of subterfuge using invisible inks or using special keywords with your friends. However, as fearless coders we have access to fancier ways to sneak data around. *evil laughter here*
The value of one pixel
There are multiple ways to hide things within other things, but today we will be working with images. A black and white image (not greyscale) is an easy thing to conceptualize, where a black pixel has a value of 1 and a white pixel as a value of 0.
Color images have three color channels (RGB), with pixel values of 0-255 for each pixel. So a pixel with the value (255,255,255) would be entirely white while (0,0,0) would be black. The upper range is 255 because it is the largest value that can be represented by an 8 bit binary number. Binary is a base-two paradigm, in contrast to decimal which is in base-ten, which means you calculate the value of a binary number by summing the 2s exponent of each place where a 1 appears.
So if we wanted to convert the number
You can also test this out in your Python interpreter. Binary numbers are automatically converted to integers so you don’t actually need to have a print statement. (It’s just there for clarity.)
From our quick tests above, you can see that the leftmost bit place matters a lot more than rightmost bit because the rightmost bit only modifies the value of the number by 1. We saw that:
Because of this, we describe the leftmost bit as the “most significant bit” (MSB) while the rightmost bit is the “least significant bit” (LSB).
We can observe that its entirely possible to hide a black and white image inside an RGB image by changing the LSB of a pixel in a single color channel to correspond to the value of the image we want to hide.
Additionally, since changing the LSB doesn’t drastically change the overall value of the of 8 bit number, we can hide our data without modifying a source image in any detectable sort of way. You can test this out with any RGB color wheel to get a sense of how little difference there is between a color like (150, 50, 50) and (151, 50, 50)
The concept of MSB and LSB occurs in other contexts as well. For example, parity bits are used as a basic form of error checking. Additionally, because the LSBs will change rapidly even if the value of the bit changes a little, they are very useful for use in hash functions and checksums for validation purposes.
Decoding the sample image
Here we have a picture of a cute dog. However, this dog is hiding a very secret message… We’re going to decode it! This image is also included in the git repo under images/encoded_sample.png.
Provided in the starter code is a function called
The Python bin function is useful in converting between integer and binary. Since bin will convert an integer to a binary string, we need to do processing on the result. Also, since the hidden information is on the red_channel only from the original RGB image, we will use the
Here’s the template code for you to fill out:
def decode_image(file_location="images/encoded_sample.png"): encoded_image = Image.open(file_location) red_channel = encoded_image.split() x_size = encoded_image.size y_size = encoded_image.size decoded_image = Image.new("RGB", encoded_image.size) pixels = decoded_image.load() for i in range(x_size): for j in range(y_size): pass #TODO: Fill in decoding functionality decoded_image.save("images/decoded_image.png")
And if you want to see the solution:
def decode_image(file_location="images/encoded_sample.png"): encoded_image = Image.open(file_location) red_channel = encoded_image.split() x_size = encoded_image.size y_size = encoded_image.size decoded_image = Image.new("RGB", encoded_image.size) pixels = decoded_image.load() for i in range(x_size): for j in range(y_size): if bin(red_channel.getpixel((i, j)))[-1] == '0': pixels[i, j] = (255, 255, 255) else: pixels[i, j] = (0,0,0) decoded_image.save("images/decoded_image.png")
Note that all our images will be in .png format. This is because JPEG format compresses the image data, which means that data encoded into the LSB may be lost.
When we run this function on the image from earlier, we see a shocking message!
Encoding a secret message
Now that we can decode secret messages, it’s only natural that we want to encode some too! Provided in the starter code are a pair of functions called
The provided code is pretty empty because it’s really similar to the
def encode_image(text_to_encode, template_image): pass #TODO: Fill out this function
But if you want the solution:
def encode_image(text_to_encode, template_image): template_image = Image.open(template_image) red_template = template_image.split() green_template = template_image.split() blue_template = template_image.split() x_size = template_image.size y_size = template_image.size #text draw image_text = write_text(text_to_encode, template_image.size) bw_encode = image_text.convert('1') #encode text into image encoded_image = Image.new("RGB", (x_size, y_size)) pixels = encoded_image.load() for i in range(x_size): for j in range(y_size): red_template_pix = bin(red_template.getpixel((i,j))) old_pix = red_template.getpixel((i,j)) tencode_pix = bin(bw_encode.getpixel((i,j))) if tencode_pix[-1] == '1': red_template_pix = red_template_pix[:-1] + '1' else: red_template_pix = red_template_pix[:-1] + '0' pixels[i, j] = (int(red_template_pix, 2), green_template.getpixel((i,j)), blue_template.getpixel((i,j))) encoded_image.save("images/encoded_image.png")
Pulling it all together
As promised, here is the script in its entirety. Hope you have fun sending hidden messages around!