[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: Decode images hidden inside other images by steganography in Python

[This program recovers an image hidden inside another image]

My post Use steganography to hide an image inside another image in Python shows how to hide one image inside another. This example shows how to recover the hidden image.

On the Encrypted tab, click Load to select the encrypted image. Then go to the Image 1 and Image 2 tabs to see the hidden and cover images respectively. Use those tabs' Save buttons to save the recovered images.

The following section explains how the program separates the two images.

Recovering Pixels

The following decode_images function separates a stegified image into its hidden and cover images.

def decode_images(image): '''Separate the two images.''' # Seed the random number generator. random.seed(1337) # Make the result images. image1 = image.copy() image2 = image.copy() # Load the images' pixels. pixels = image.load() pixels1 = image1.load() pixels2 = image2.load() # Find the bounds we need to process. wid = image.width hgt = image.height # Process. mask1 = 0b00001111 mask2 = 0b11110000 for y in range(hgt): for x in range(wid): r, g, b = pixels[x, y] r1 = ((r & mask1) ^ random.randint(0, 15)) << 4 g1 = ((g & mask1) ^ random.randint(0, 15)) < 4 b1 = ((b & mask1) ^ random.randint(0, 15)) < 4 r2 = r & mask2 g2 = g & mask2 b2 = b & mask2 pixels1[x, y] = (r1, g1, b1) pixels2[x, y] = (r2, g2, b2) return image1, image2

Remember in my previous post how the encryption program initialized its random number generator with the seed 1337? (If you don't remember, go look at that post now.) That program used random numbers to randomize the hidden image's pixel values. To unrandomize those values, this program needs to use the same seed, so this function starts by initializing the random number generator with the seed 1337.

Next, the program makes two copies of the input image. It won't use the pixels in those copies, this is just an easy way to make two new images with the same size as the original.

The code then loads the original and result images so it's easy to work with their pixels. The function gets the images' width and height.

The function creates two masks, one to clear the four least significant bits (LSBs) and one to clear the four most significant bits (MSBs) of an 8-bit integer. Finally, the code loops through the image's pixels.

You may recall that my previous post used the following calculation to find the encrypted image's red color component. (Although I doubt you remember given that you didn't remember the things about initializing the random number generator. Come on, show some effort!)

r2 = (r2 & mask) | ((r1 >> 4) ^ random.randint(0, 15))

This calculation takes the cover image component r2 and uses a mask to clear its LSBs. It then takes the hidden image's color component r1, shifts it right by four bits to discard its LSBs and move the MSBs into the LSB positions. It combines that result with a random 4-bit number between 0 and 15 to obscure the hidden message's colors. Finally, it combines the cover image's MSBs with the modified hidden image's LSBs.

The new decode_images function uses the following calculation to recover the hidden image's red color components.

r1 = ((r & mask1) ^ random.randint(0, 15)) << 4

The first statement uses mask1 to clear the MSBs in the input pixel's red color component leaving only the bits from the hidden image. It then uses ^ (XOR) to remove the 4-bit random number from the result. Because this program initialized the random number generator the same way the stegification function did in my previous post, the random number here is the same one as before. Combining a number with ^ twice essentially removes the random number leaving the original hidden image color component in the four LSBs. The statement then left shifts the result to move the LSBs to the MSB positions and we have the recovered hidden image red color component.

After it repeats those steps to get the hidden image's green and blue components, the function uses the following calculation to recover the cover image's red color component.

r2 = r & mask2

This statement simply uses mask2 to clear the component's LSBs leaving the cover image's red component. The function uses similar statements to recover the cover image's green and blue components.

After it finds the color components, the code saves them in the new result images.

When it has processed all of the image's pixels, the function returns the hidden and cover images.

Losing Information

The stegification process removes the four LSBs from both the cover and hidden image and those bits are lost forever. When you recreate the images, the LSBs are all zeros so the result images aren't quite the same as the originals. The difference is very hard to detect, though, particularly if the images are photographs or other images that don't have large areas of uniform color.

If you like and the cover image is larger than the hidden image, you can use multiple result pixels to store each hidden image pixel. For example, instead of storing four hidden image bits in each pixel, you could store only two. That would let you keep six cover image bits instead of only four. If you also want to store six bits of the hidden image, you would need to use three result pixels (each holding two hidden bits) per hidden image pixel. That means the cover image would need to be at least three times as bit as the hidden image.

The following table shows the number of colors each image could represent with different numbers of pixels.

Bits Per Component 8 6 4
# Colors 16,777,216 262,144 4,096

Using 8-bit color (eight bits per color component) gives you 2 ^ 24 = 16,777,216 possible colors. The average human eye can only distinguish about a million shades, so that's far more than enough.

The 2 ^ 18 = 262,144 colors given by 6-bit color is plenty to make the result very close to the original. Even the 2 ^ 12 = 4,096 colors given by 4-bit color produces a very good result as you can see from the picture at the top of this post.

Conclusion

You can use the previous example to stegify images and use this one to recover the hidden and cover images. These two examples use Python's random module, initializing it with the seed 1337. That means this method relies on "security through obscurity." An attacker who knows how you're stegifying the image can easily destegify it.

In my next post, I'll explain how you can add true encryption to the process to make the hidden image completely secure from prying eyes.

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

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