0

I was trying to create an example of stegnography techniques in python by creating a program that converts an image to its binary with the python PIL module. Once converted it would strip the least significant bit from each pixel value and change it to encode a message across a certain number of bytes; it would do this whilst (theoretically) keeping the approximate size of the file and maintaining most of the image's appearance.

I have however, run into a problem whereby I cannot reconstruct the image or any image at all for that matter using the data I am left with.

#secret message to be injected into the final bits.
injection = '0111001101100101011000110111001001100101011101000010000001101101011001010111001101110011011000010110011101100101001000000110100101101110011010100110010101100011011101000110100101101111011011100010000001101101011011110110110101100101011011100111010000100000011010000110010101101000011001010110100001100101011010000110010101101000011001010110100001100101'
injection_array=[]


count = 0
#loop to create an array of characters for the injection
for char in injection:
    injection_array.append(injection[count])
    count += 1
injectioncount = 0
count = 0
#loop to replace each bit with the desired injection
for element in pixel_bin:
    cached_element = pixel_bin[count]
    lsb_strip = cached_element[:-1]
    lsb_replace = lsb_strip + injection_array[injectioncount]
    pixel_bin[count] = lsb_replace
    count += 1
    injectioncount += 1
    if injectioncount == len(injection):
        break

#dumps outputted value for comparison with a control file of original pixel values
with open("comparison.json","w") as f:
    json.dump(pixel_bin,f, indent=2)

And this is as far as I have managed to get. The output from this is a list containing varying sequences of binary with the final bit changed.

This is how I am getting the pixel data in the first place:

def bincon(image):

from PIL import Image
count = 0
pixel_binaries = []
im = Image.open(image, 'r')

pix_val = list(im.getdata()) #outputs rgb tuples for every pixel
pix_val_flat = [x for sets in pix_val for x in sets] #flattens the list so that each list item is just one rgb value

for elements in pix_val_flat: #converts each rgb value into binary
    pixel_binaries.append(bin(pix_val_flat[count]))
    count += 1

return pixel_binaries

How would I go about reconstructing the data I have gathered into something resembling the image I have input into the program?

EDIT - trying the im_out = Image.fromarray(array_data_is_stored_in) method does return an image file which opens but contains no data, certainly not the original image.

6
  • im_out = Image.fromarray(array_data_is_stored_in) if your data is stored in the appropriately shaped 2D array. They might have to be Numpy arrays as well. Commented Sep 29, 2022 at 13:42
  • Hi when I tried to do this it returned the error ``` TypeError: Cannot handle this data type: (1, 1), <U8 ``` which is because my data is 1 dimensional. How would i go about making an array this big (4,000,000) elements into 2 dimensions. Commented Sep 29, 2022 at 14:49
  • next question: stackoverflow.com/questions/73898109/… Commented Sep 29, 2022 at 15:37
  • That error is about data type, not about dimensions! Commented Sep 29, 2022 at 15:56
  • I don’t understand the code you posted. for element in pixel_bin:, but then you don’t use element in the loop body. You also don’t define pixel_bin anywhere. I recommend that you use a NumPy array to represent your image, and use NumPy operations on it. Finally, what is the value of '0'? Is it 0 or is it 48? Commented Sep 29, 2022 at 17:05

1 Answer 1

1

The code that you posted was not a minimal reproducible example so it was difficult to diagnose what the issue was.

However the basic approach is to read the image into a Numpy array. The array will be an x,y (2d) array for each of the layers/colors.

Numpy has a flatten() and reshape() methods on the arrays. This means that you can flatten the array before iterating over it to change each pixel. Then reshape it back after the modification.

An example of what that might look like:

from pathlib import Path
from typing import List

import imageio.v3 as iio
import numpy as np

secret_bit = 0


def max_msg_len(data: np.ndarray) -> int:
    row_size, column_size, layers = data.shape
    return (row_size * column_size * layers) // 8


def text_to_bits(txt: str) -> List[bool]:
    data = txt.encode()
    bin_str = "".join([f"{x:08b}" for x in data])
    bits = [x == "1" for x in bin_str]
    return bits


def bits_to_text(bits: List[bool]) -> bytes:
    data = bytearray()
    for x in range(0, (len(bits) // 8 * 8), 8):
        char = int("".join(["1" if i else "0" for i in bits[x: x + 8]]), 2)
        data.append(char)
    return_str = "".join([x for x in data.decode() if x.isprintable()])
    return return_str


def read_file(filename: Path) -> np.ndarray:
    return iio.imread(filename)


def write_file(filename: Path, data: bytes) -> None:
    iio.imwrite(filename, data)


def extract_message(data: np.ndarray) -> List[bool]:
    flat_data = data.flatten()
    extracted_bits = []
    for pixel in range(len(flat_data)):
        value = flat_data[pixel]
        found_bit = get_bit(value, secret_bit)
        extracted_bits.append(found_bit)
    return extracted_bits


def embed_message(data: np.ndarray, bits: List[bool]) -> bytes:
    orig_shape = data.shape
    flat_data = data.flatten()
    idx = 0
    bit_len = len(bits)
    for pixel in range(len(flat_data)):
        value = flat_data[pixel]
        if pixel < bit_len:
            flat_data[pixel] = set_bit(value, secret_bit, bits[pixel])
        else:
            flat_data[pixel] = set_bit(value, secret_bit, False)
        idx += 1
    return flat_data.reshape(orig_shape)


def set_bit(octet: int, bit: int, value: bool) -> int:
    mask = 1 << bit
    octet &= ~mask
    if value:
        octet |= mask
    return octet


def get_bit(octet: int, bit: int) -> bool:
    value = octet >> bit
    value &= 1
    return bool(value)


def main(img_in: Path, img_out: Path, msg: str) -> None:
    print("Message to hide:", msg)
    msg_bits = text_to_bits(msg)
    # Read img file
    img_content = read_file(img_in)
    # Check message will fit in image file
    # print(max_msg_len(img_content), len(msg), msg_bits)
    if len(msg) > max_msg_len(img_content):
        print(
            f"Message too long for image file. "
            f"Can only have {max_msg_len(img_content)} characters"
        )
        exit()
    # Modify data
    new_data = embed_message(img_content, msg_bits)
    # Check message can be extracted
    extracted_bits = extract_message(new_data)
    print("Message in new image:", bits_to_text(extracted_bits))
    # Save to file
    write_file(img_out, new_data)
    # Read from secret file
    img_content = read_file(img_out)
    read_bits = extract_message(img_content)
    print(f"From {img_out.name} read: {bits_to_text(read_bits)}")


if __name__ == "__main__":
    # Files
    data_dir = Path(__file__).parent.joinpath("data")
    original_png = data_dir.joinpath("gnome-xterm.png")
    secret_png = data_dir.joinpath("secret.png")
    # Message
    txt_to_hide = "secret message to hide hehehe"
    main(original_png, secret_png, txt_to_hide)

This gave the following output in the terminal:

Message to hide: secret message to hide hehehe
Message in new image: secret message to hide hehehe
From secret.png read: secret message to hide hehehe

The before and after images were:

enter image description here

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.