Created
January 7, 2024 10:41
-
-
Save SimenZhor/cf9913466501eaeec2138ab0f2f07ad2 to your computer and use it in GitHub Desktop.
LSB Steganography
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| 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