I am trying to implement a basic stenography technique in which I am replacing the LSB of the carrier image with the MSB of the message image. The LSB can belong to any of the RGB channel.
My approach is quite naive as I am looping the message_matrix and storing its MSB of a particular RGB channel in the corresponding LSB of the carrier_matrix. As the image size is not more than 1024 * 1024 the time complexity of performing this operation is O(n^2) but since I am using python the time taken is very high as compared to Java.
Can the same operation be performed in a more optimised way ?
def hide_using_bpcs(self, carrier_path, message_path, index, color_index):
carrier_matrix = self.image_to_matrix(carrier_path)
message_matrix = self.image_to_matrix(message_path) #use np.zeros
for row_index, row in enumerate(message_matrix):
for pixel_index, pixel in enumerate(row):
color = message_matrix[row_index][pixel_index][color_index]
msb = (color & 0xff) >> 7
carrier_pixel = carrier_matrix[
row_index][pixel_index][color_index]
carrier_matrix[row_index][pixel_index][
color_index] = self.set_bit(carrier_pixel, index, msb)
stegano_image = self.matrix_to_image(carrier_matrix)
return stegano_image
Now, for displaying a particular bit plane let say (Red 0), I am setting all the values of Green and Blue plane as 0 and retaining only the value of LSB (or the 0 bit) of the red color in the image. I have gone through some implementations done using openCV like [b,g,r = cv2.split(img)] but this is only splitting the image in 3 channels. What I want is to split a particular channel let say Red further into 8 Variations by retaining the value at the corresponding position.
def display_bit_plane(self, path, color_index, color_bit):
matrix = self.image_to_matrix(path)
matrix = matrix.astype(int)
result_matrix = self.image_to_matrix(path)
mask = 1 << color_bit
for row_index, row in enumerate(matrix):
for pixel_index, pixel in enumerate(row):
for iterator in range(0, 3):
result_matrix[row_index][pixel_index][iterator] = 0
color = matrix[row_index][pixel_index][color_index]
result_matrix[row_index][pixel_index][color_index] = self.set_bit(0, 7, ((color & mask) != 0))
stegano_image = self.matrix_to_image(result_matrix)
return stegano_image
I am using NumPy array for performing all the computations. However iterating it in usual way is very costly. Please provide some optimisation in the above two functions, so that these operations can be done in less than 1 second.
Edit 1 :
I have optimised the second function of retrieving the bit plane. If it can be further simplified please do tell. Color_index represents R, G, B as 0, 1, 2 respectively and color_bit is the bit position from 0-7.
def display_bit_plane_optimised(self, path, color_index, color_bit):
message_matrix = self.image_to_matrix(path)
change_index = [0, 1, 2]
change_index.remove(color_index)
message_matrix[:, :, change_index] = 0
mask = 1 << color_bit
message_matrix = message_matrix & mask
message_matrix[message_matrix == 1] = 1 << 7
stegano_image = self.matrix_to_image(message_matrix)
return stegano_image
Anything that applies to the whole array can be vectorised. If you want to apply an operation only on a part of the array, slice it.
I'm providing complete code so not to make assumptions about image_to_matrix()
and matrix_to_image()
methods. Take it from there.
I've kept your logic intact otherwise, but if you're only intending to embed the secret in the LSB, you can ditch pixel_bit
, set its value to zero and simplify whatever constants result out of it. For example, in embed()
you'd simply get mask = 0xfe
, while any bitshifts by 0 are inconsequential.
import numpy as np
from PIL import Image
class Steganography:
def embed(self, cover_file, secret_file, color_plane, pixel_bit):
cover_array = self.image_to_matrix(cover_file)
secret_array = self.image_to_matrix(secret_file)
# every bit except the one at `pixel_bit` position is 1
mask = 0xff ^ (1 << pixel_bit)
# shift the MSB of the secret to the `pixel_bit` position
secret_bits = ((secret_array[...,color_plane] >> 7) << pixel_bit)
height, width, _ = secret_array.shape
cover_plane = (cover_array[:height,:width,color_plane] & mask) + secret_bits
cover_array[:height,:width,color_plane] = cover_plane
stego_image = self.matrix_to_image(cover_array)
return stego_image
def extract(self, stego_file, color_plane, pixel_bit):
stego_array = self.image_to_matrix(stego_file)
change_index = [0, 1, 2]
change_index.remove(color_plane)
stego_array[...,change_index] = 0
stego_array = ((stego_array >> pixel_bit) & 0x01) << 7
exposed_secret = self.matrix_to_image(stego_array)
return exposed_secret
def image_to_matrix(self, file_path):
return np.array(Image.open(file_path))
def matrix_to_image(self, array):
return Image.fromarray(array)
When I run it, it all completes within a second.
plane = 0
bit = 1
cover_file = "cover.jpg"
secret_file = "secret.jpg"
stego_file = "stego.png"
extracted_file = "extracted.png"
S = Steganography()
S.embed(cover_file, secret_file, plane, bit).save(stego_file)
S.extract(stego_file, plane, bit).save(extracted_file)
Notes
Your display_bit_plane_optimised()
was reasonably optimised, but it had a bug if color_bit
was anything but 0. The line
message_matrix = message_matrix & mask
zeros every other bit, but unless color_bit
is 0, the values will be some other power of 2. So when you come to
message_matrix[message_matrix == 1] = 1 << 7
no pixel is modified. If you want to keep your way, you have to change the last line to
message_matrix[message_matrix != 0] = 1 << 7
My approach was simply to bring the embedded bit to the LSB position, zero out every other bit and then shift it to the MSB position with no conditionals.