Image Steganography in Python

Second Title: Hiding Secret Messages in Cute Pictures of Dogs

What is this pupper hiding???

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 steganography.py and has some missing functions for you to fill out, while the full solution is in solution.py. There are two main functions to this code. The first is a decoding function that can extract secret information from an image file, while the second is a function that can encode secret messages into images.

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 solution.py. It is also embedded at the end of this post.

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:

Since everyone can read, encoding text in neutral sentences is definitely effective

turns into

Secret inside

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 10001011 from binary into decimal, it would look something like:

2^8 + 2^4 + 2^2 + 2^1 = 139

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.)

>>> print(0b10001011)
139
>>> type(0b10001011)
<class ‘int’>
>>> 0b00001011
11
>>> 0b10001010
138

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:

10001011 = 139 while 00001011 = 11
10001011 = 139 while 10001010 = 138

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)

Aside
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.

Image Source: DogBreedInfo.com

Provided in the starter code is a function called decode_image(). The secret image was hidden in the LSB of the pixels in the red channel of the image. That is, the value of the LSB of each red pixel is 1 if the hidden image was 1 at that location, and 0 if the hidden image was also 0. So, we will need to iterate though each pixel in the encoded image and set the decode_image pixel to be (0, 0, 0) or (255, 255, 255) depending on the value of that LSB.

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 .split() function to isolate the red channel only.

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()[0]

    x_size = encoded_image.size[0]
    y_size = encoded_image.size[1]

    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()[0]

    x_size = encoded_image.size[0]
    y_size = encoded_image.size[1]

    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!

Alllll the memessss

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 write_text() and encode_image(). The first will take a string and convert it to a black and white image of the string. We’re going to use it as a helper function in our secret message encoding shenanigans.

The provided code is pretty empty because it’s really similar to the decode_image() function from earlier:

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()[0]
    green_template = template_image.split()[1]
    blue_template = template_image.split()[2]

    x_size = template_image.size[0]
    y_size = template_image.size[1]

    #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!

-Sophie

22 Comments

    • Sophie

      Sorry that I’m about 2 years late to answering this, but grey scale is essentially RGB but with only 1 channel. You can hide data in that channel identically to the example I have above.

  1. Sagmac

    Even when i am changing the encoded message,it keeps showing me the default message no matter how many times i change it.
    My encoded message just won’t show up
    PLZ send help

    • Sophie

      Hi Sagmac, I’ll need to see some of your code to figure out what’s happening. Maybe you’re not pointing the decoder at the correct image file?

  2. get

    is there any one who has a full source code of a Steganography i really want the source code
    “gettse29@gmail.com”

    • Sophie

      It’ll depend on how the resizing and cropping is done. Because the hidden message is simply layered into the cropped, if there isn’t too much distortion the message will still remain.

      • PRANSHOO VERMA

        If the image gets cropped then the encoding will be destroyed. Then which steganography can be done to get out of this problem? And how to implement it?

  3. satadhi halder

    it will be really great if you start a youtube series regarding this topic ! there are not many series dedicated to stegenography . thanks

  4. Paul

    Hey, thanks for sharing your code, I’ve had a great time doing the exercise. Do you know any interesting materials to expand in this area? It’s super-easy when comparing to C-like languages and I really enjoyed this one.

    • Sophie

      Hi Paul, I’m glad you liked it! Python is def the most enjoyable language for me to play around in because implementations are usually pretty straight forward. As for exercises, the students at Olin learn python via the Think Python book: http://greenteapress.com/wp/think-python-2e/. The later chapters have some more sophisticated problems to solve.

      I’ve done a couple fun exercises using python with the Speech to Text and the Knight’s Tour problem, so you could try to implement those in a better way / with more features. Otherwise, feel free to check back since it is a goal of mine to do more tutorial/exploratory style posts.

      • Paul

        Thank you for the response!
        I’d love to read more of your articles. I’ll definitely check out this book, thanks for that too.

    • Sophie

      I don’t know exactly how to implement such an application, but you should be able to integrate the python code within the django framework fairly easily as it doesn’t rely on too many external libraries.

  5. Nitin Shukla

    Thanks for the precisely written blog on the subject. If possible please enlighten me on ‘How to retrieve the encoded message as text from the decoded image’.

    • Sophie

      Hi Nitin,

      I’m not quite sure what you mean. The decode_solution function does extract the hidden message from the image with an encoded message though. Can you clarify what your confusion is?

        • Sophie

          Ahh, okay. To do that, remember that ASCII is text rendered from 8 bit encoding. You can encode the text into the image the same way you’re encoding another image in a bunch of ways. For example, the letter ‘h’ is ‘01110011’ in binary and ‘i’ is ‘1110100’. Treat those numbers as the 1s and 0s you’re hiding in the LSB of the pixel and you can directly hide ASCII text. Decoding the message will be similar, but you’ll have to remember to look at the decoded values in groups of 8 to pull the ASCII text back out from the encoded image.

Leave a Reply

Your email address will not be published. Required fields are marked *