Skip to content

Instantly share code, notes, and snippets.

@SimenZhor
Created January 7, 2024 10:41
Show Gist options
  • Select an option

  • Save SimenZhor/cf9913466501eaeec2138ab0f2f07ad2 to your computer and use it in GitHub Desktop.

Select an option

Save SimenZhor/cf9913466501eaeec2138ab0f2f07ad2 to your computer and use it in GitHub Desktop.
LSB Steganography
"""
Simple script that encodes an image into another image by hijacking the two least significant bits.
The script has only been tested on images that have the same dimensions and I've only done a half-hearted attempt at matching the image sizes.
"""
from PIL import Image
import numpy as np
import os
def extract_secret_image(filepath, output_filepath = None):
with Image.open(filepath) as original_image:
original_image_arr = np.asarray(original_image)
# Extract two LSB and scale so that 0b11 becomes 255
hidden_image_arr = np.bitwise_and(original_image_arr, 0b00000011) * 85
hidden_image = Image.fromarray(hidden_image_arr)
# Show and save hidden image
hidden_image.show()
if output_filepath is not None:
hidden_image.save(output_filepath)
return hidden_image
def convert_img_to_6_bit_palette(image):
# Create a palette that can be represented exactly by 6 bit color depth (2 for each R, G, and B)
palette = []
# Shades that are representable with 2 LSB encoding
shades = [0b00, 0b01 * 85, 0b10 * 85, 0b11 * 85] # scaling by 85 is only done for debugging purposes, so the result can be inspected visually.
# Iterate over R,G,B
for r in range(len(shades)):
for g in range(len(shades)):
for b in range(len(shades)):
palette.append(shades[r])
palette.append(shades[g])
palette.append(shades[b])
p_img = Image.new('P', (16, 16))
p_img.putpalette(palette)
result = image.quantize(colors=64, palette=p_img)
return result
def encode_secret_image_into_main_image(main_image, secret_image, outfile):
with Image.open(main_image) as main_im:
if main_im.mode == "RGBA":
# Discard alpha channel for simplicity
main_im = main_im.convert("RGB")
with Image.open(secret_image) as secret_im:
# Half-hearted attempt at size matching. Definitively won't work for all cases.
if secret_im.size != main_im.size:
# Prioritize height
factor = int(secret_im.height / main_im.height)
secret_im = secret_im.reduce(factor)
# Crop largest image to fit
if secret_im.width >= main_im.width:
secret_im.resize(main_im.size)
elif main_im.width >= secret_im.width:
main_im.resize(secret_im.size)
# Extract main image data and prepare output buffer of same size
main_image_arr = np.asarray(main_im)
out_arr = np.zeros(main_image_arr.shape).astype("uint8")
# Convert secret image to 6-bit color palette and enforce RGB (for simplicity)
six_bit_secret = convert_img_to_6_bit_palette(secret_im)
if six_bit_secret.mode == "RGBA" or six_bit_secret.mode == "P":
six_bit_secret = six_bit_secret.convert("RGB")
# Extract secret image data
secret_arr = np.asarray(six_bit_secret)
secret_arr = np.round(secret_arr / 85).astype("uint8") # Normalize to 0b11 mask (see comment in convert_img_to_6_bit_palette function about the 85 factor)
# Overwrite the two LSB of the main image with the 6-bit color depth secret image.
out_arr = np.bitwise_and(main_image_arr, 0b11111100)
out_arr = np.bitwise_or(out_arr, secret_arr)
# Save result
out_image = Image.fromarray(out_arr)
out_image.save(outfile)
if __name__ == '__main__':
main_img = r"./main_img.png"
hidden_img = r"./rickroll.jpg"
encoder_output_filename = r"./encoded_imgs/secret_rickroll.png"
recovered_path = r"./recovered_secret.png"
encode_secret_image_into_main_image(main_image=main_img, secret_image=hidden_img, outfile=encoder_output_filename)
extract_secret_image(encoder_output_filename, output_filepath=recovered_path)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment