Search code examples
pythonopencvnumpysteganography

How to hide one image into another one using LSB substitutuion?


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

Solution

  • 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.