Reputation: 9
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.
Upvotes: 0
Views: 694
Reputation: 7924
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:
Upvotes: 1