[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky] [Facebook]
[Build Your Own Python Action Arcade!]

[Build Your Own Ray Tracer With Python]

[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

Title: Use steganography to hide an image inside another image in Python

[This program hides an image inside another image]

My post Use steganography to hide messages in an image in Python shows how you can hide a message inside an image. To hide a message image in a cover image, you can use a similar approach: hide the message image's pixel values bit-by-bit in the cover image.

That approach has two major drawbacks. First, it's confusing. The each pixels has three color components, each of which occupies eight bytes. That means you need to keep careful track of where each bit goes. You can use an approach similar to the bit extractor in the previous example, but it's some additional work.

The second drawback is that encrypting this way uses a lot of pixels in the cover image. Each message pixel has three color components, each using 8 bits, so encoding each pixel in the message image takes up 24 pixels in the cover image.

Fortunately, there's an approach that's easier, uses far fewer cover image pixels, and that produces an acceptable result. The final image isn't completely perfect, but it's pretty good, particularly for photographs where there are few large areas of uniform color.

Hiding Pixels

The previous example stores each bit in a message in the least significant bit of the cover image's pixels. That bit makes so little difference to the final picture that you can't tell it's there. If you encrypt the message (see my post Use steganography to hide encrypted messages in an image in Python), the bits are randomized so it's extremely hard to tell that the least significant bit was altered.

A few years ago, I followed that approach to hide message images, putting individual message bits in a cover image. Then, to save space, I tried using the cover image's two least significant bits. To my slight surprise, I found that you still couldn't tell the difference. I then tried the three and finally the four least significant bits. In all cases, it made surprisingly little difference!

To see why, think about how many colors you can represent with only four bits per component. With four bits for each of three components, you can create 2 ^ 12 = 4,096 values. That means using three 4-bit components you can creates 4,096 different colors. That's far fewer than the 2 ^ 24 = 16,777,216 colors you can define with 8-bit components, but it's still a lot. Unless you compare before and after images very closely, you're unlikely to notice the difference.

So that's the approach taken by this example: store the four most significant bits (MSBs) from the message image in the four least significant bits (LSBs) of the cover image. The result is pretty good and it only requires one cover pixel per message pixel.

Hiding the Image

The following encode_image function hides one image inside another.

def encode_image(image1, image2): '''Encode image1 in image2.''' # Make a copy so we don't mess up the original. image2 = image2.copy() # Load the images' pixels. pixels1 = image1.load() pixels2 = image2.load() # Find the bounds we need to process. wid = min(image1.width, image2.width) hgt = min(image1.height, image2.height) # Process. mask = 0b11110000 for y in range(hgt): for x in range(wid): r1, g1, b1 = pixels1[x, y] r2, g2, b2 = pixels2[x, y] r2 = (r2 & mask) | (r1 >> 4) g2 = (g2 & mask) | (g1 >> 4) b2 = (b2 & mask) | (b1 >> 4) pixels2[x, y] = (r2, g2, b2) return image2

The function first copies the cover image so we don't mess up the original. It then loads both images so the code can easily manipulate their pixels.

Next, the code finds the minimum width and height of the two images. You can try to encode images of differ shapes if you like, but it's much easier to just deal with the area where they overlap.

The then code defines a bit mask that it can use to clear the least significant bits of an 8-bit byte. You'll see how that works in a second.

Now the code loops through the pixels in the area of image overlap. For each pixel, it gets the red, green, and blue color components from both images. The r1, g1, b1 values are the message image's components and the r2, g2, b2 components are the cover image's components.

Next, the code calculates the result pixel's components. For example, consider this calculation:

r2 = (r2 & mask) | (r1 >> 4)

The r2 & mask part combines the r2 value with the bit mask. It uses the & (AND) operator so the result contains the same bit values in r2 only where the mask has a 1. Where the mask has a 0, the result has a 0. That preserves the values MSBs while zeroing its LSBs.

The r1 >> 4 part of the calculation shifts the r1 value 4 bits to the right. That drops the four LSBs, moves the four MSBs into the LSB positions, and pads the result on the left with zeros in the MSBs.

Finally, the calculation combines the two values using the | (OR) operator so the end result has the r2 MSBs in the result MSBs and the r1 MSBs in the result LSBs.

Here's a quick example. Suppose r1 (the message component) has bits abcdefgh and t2 (the cover component) has bits ABCDEFGH.

The r2 & mask step gives ABCD0000.

The r1 >> 4 step gives 0000abcd.

Combining those with | gives the final result ABCDabcd.

The function repeats those steps to find the new green and blue components, sets the result pixel to those values, and Bob's your uncle.

When it's done, the function returns the modified copy of the cover image.

Reviewing the Results

[Without randomization, you can sometimes see a ghostly copy of the message image] This method works well for photographs and other images that don't have large areas of solid color, but the message image can be noticeable sometimes, particularly of the cover image has large dark areas. For example, the picture on the right shows a greatly enlarged picture hiding the cover of my book Build Your Own Python Action Arcade! inside the cover of my book Build Your Own Ray Tracer With Python. Above the yellow "R" you can see a ghostly "B" and part of a "u" from the hidden image. If you run the example and look very closely at the results, you can see more of the hidden message.

Improving the Results

[Randomization hides the message ghost] One way to exorcise the ghostly message image, is to randomize the message bits. That removes any large areas of uniform color and creates slight modifications as shown in the picture on the right.

Here's the new version of encode_image with the modifications highlighted in blue.

def encode_image(image1, image2): '''Encode image1 in image2.''' # Seed the random number generator. random.seed(1337) # Make a copy so we don't mess up the original. image2 = image2.copy() # Load the images' pixels. pixels1 = image1.load() pixels2 = image2.load() # Find the bounds we need to process. wid = min(image1.width, image2.width) hgt = min(image1.height, image2.height) # Process. mask = 0b11110000 for y in range(hgt): for x in range(wid): r1, g1, b1 = pixels1[x, y] r2, g2, b2 = pixels2[x, y] r2 = (r2 & mask) | ((r1 >> 4) ^ random.randint(0, 15)) g2 = (g2 & mask) | ((g1 >> 4) ^ random.randint(0, 15)) b2 = (b2 & mask) | ((b1 >> 4) ^ random.randint(0, 15)) pixels2[x, y] = (r2, g2, b2) return image2

The calculation is pretty much as before with two changes. First, to make the operation repeatable (which is essential if you ever want to retrieve the message image), the function sets the random number generator's seed to 1337. It doesn't matter what seed value you use, as long as you remember it. (You'll see why in my next post.)

Second, the code combines the message's LSBs with a random integer between 0 and 15 (a 4-bit integer) using the ^ (XOR) operator. That scrambles the LSBs in the result pixel so the message image is randomized and doesn't show through.

Conclusion

Use the Load buttons on the program's Image 1 and Image 2 tabs to load the images. Then use the Save buttons on the other tabs to save the images hidden inside each other.

The following table shows the original images and the images hidden inside each other with and without randomization. It's pretty hard to tell the difference between the three versions unless you enlarge the images.

Original
Images
Without
Randomization
With
Randomization

This example shows how to hide one image inside another. In my next post, I'll explain how you can recover the hidden image.

Meanwhile, download the example to experiment with it and to see additional details.

© 2025 - 2026 Rocky Mountain Computer Consulting, Inc. All rights reserved.