Steganography Part-2

Rahul Kumar Patro
5 min readJan 18, 2021

Learn how to hide Images Inside Images

We saw how to hide information inside Images, now it's time to see another form of Steganography.

Image Steganography

Hiding an Image Inside an Image.

As our goal is to hide an image inside another image, one way we can do this is by converting the images to binary and then store the converted binary bits using the Least Significant Bit Steganography.

For more details about the Least Significant Bit Steganography, you can refer to my previous post.

Now let’s proceed to the code part: —

Step-1:

Loading the two images and ensuring the image to which another image will be merged is bigger in size than the other. If not then resize it accordingly.

The Image which is to be merged:

The image on which the above image will be merged:

Step-2:

Below is the code for the integer to binary conversion where the input will be the pixel value that itself is a tuple of r,g,b.

def int2bin(rgb):

#Will convert RGB pixel values from integer to binary
#INPUT: An integer tuple (e.g. (220, 110, 96))
#OUTPUT: A string tuple (e.g. ("00101010", "11101011", "00010110"))

r, g, b = rgb
return ('{0:08b}'.format(r),
'{0:08b}'.format(g),
'{0:08b}'.format(b))
#Return converted r,g,b binary values separately..

And for binary to integer conversion where the input will be a binary number and results to an integer.

def bin2int(rgb):

#Will convert RGB pixel values from binary to integer.
#Reverse of the first part.

r, g, b = rgb
return (int(r, 2),
int(g, 2),
int(b, 2))
#return converted r,g,b integer values separately

Step-3:

For merging the two images we can replace the last two bits of each r,g,b value with that of the new image. You can choose this yourself ensuring you are not losing any content from the original image.

def merge2rgb2(rgb1, rgb2):

r1, g1, b1 = rgb1
r2, g2, b2 = rgb2
rgb = (r1[:6] + r2[:2],
g1[:6] + g2[:2],
b1[:6] + b2[:2])
return rgb

Step-4:

Merge Function for merging the two Images:

def merge2img2(img1, img2):

image1=img1
image2=img2
#print('rahul')

# Condition for merging
if(image1.size[0]>image2.size[0] or image1.size[1]>image2.size[1]):
print("Cannot merge as the size of 1st Image is greater than size of 2nd Image")
return

# Getting the pixel map of the two images
pixel_tuple1 = image1.load()
pixel_tuple2 = image2.load()

#print(pixel_tuple1)
#print(pixel_tuple2)

# The new image that will be created.
new_image = Image.new(image2.mode, image2.size) # Setting the size of Image 2 as Image 1 will be merged to Image 2.
pixels_new = new_image.load()

for row in range(image2.size[0]):
for col in range(image2.size[1]):

rgb1 = int2bin(pixel_tuple2[row, col])

# Using a black pixel as default
rgb2 = int2bin((0, 0, 0))

# Converting the pixels of image 1 if condition is satisfied

if(row <image1.size[0] and col< image1.size[1]):
rgb2= int2bin(pixel_tuple1[row,col])


merge_rgb= merge2rgb2(rgb1,rgb2)

pixels_new[row,col] = bin2int(merge_rgb)

#print('rahul')
new_image.convert('RGB').save('merged2.jpg')

return new_image

First checking with the size compatibility then iterating over the image on which another image will be merged and converting the integer pixel values to binary and replacing it will the binary converted values of the image to be merged. Finally converting the changed binary number to an integer.

After merging the image the merged image was looking like this:

Can you see any difference between the original and the merged image??

Step-5:

Function for unmerging the image:

def unmerge2(img):

pixel_map = img.load()

new_image = Image.new(img.mode, img.size)
pixels_new = new_image.load()


original_size = img.size

for row in range(img.size[0]):
for col in range(img.size[1]):
r, g, b = int2bin(pixel_map[row, col])

# Extracting the last 6 bits (corresponding to the hidden image) and adding zeroes to increase the brightness.

rgb = (r[6:] + "000000",
g[6:] + "000000",
b[6:] + "000000")

# Convert it to an integer tuple
pixels_new[row, col] = bin2int(rgb)

#If this is a 'valid' position, store it as a last valid option
if pixels_new[row, col] != (0, 0, 0):
original_size = (row + 1, col + 1)

# Crop the image based on the 'valid' pixels
new_image = new_image.crop((0, 0, original_size[0], original_size[1]))

return new_image

Here the reverse process will be followed i.e iterating over the merged image and restoring the image that was merged by taking the last 2 bits of every r,g,b value and then adding zeroes to it for good brightness.

We will continue to do that until we find an invalid position.

After unmerging the unmerged image was looking like this:

Can you see any difference between the original and the unmerged image??

Results:

We can see the final unmerged image is slightly less bright than the original but mostly the same as that of the previous original image.

Previously I tried merging 4 bits out of 8 bits but that was not successful as the image after unmerging the result was not quite good.

So we have concluded that the 2 MSBs of Image 1 were more important than 4 MSBs of image 1, they were adding noise to the image. So merging 6 MSBs of Image2 and 2 MSBs of Image 1 was helpful.

If you are interested in the code, you can find my notebook on Github.

To visit the first part of this series i.e Text Steganography please visit this link.

Hope you enjoyed the article!!

--

--