Embed every bit of every pixel of the secret image into the cover image

First, I read the cover image as greyscale image.

coverImage = cv2.imread(coverImagePath, 0)

Then I pass the cover image to another function to create another smaller image and that smaller image is my secret image.

def makeSecretImage(coverImage):
    copyImage = coverImage.copy()
    secretImage = cv2.resize(copyImage, (100,100))
    return secretImage

After that I pass the two images to my embedding function to embed the secret image into the cover image. My intention is to embed every bit of every pixel of the secret image to the LSB of the cover image and the LSB of the cover image is selected randomly. The final output of this function is a stego image which will looks exactly same with the cover image as it only change the LSB of the pixel.

def embedding(coverImage,secretImage, password):
    indices = permute_indices(coverImage.shape, password)
    for i in range (secretImage.shape[0]):
        for j in range(secretImage.shape[1]):
            x,y = next(indices)
            #convert pixel to binary 
            pixels_cover = format(coverImage[x][y], '08b')
            pixels_hide = format(secretImage[i][j], '08b')
            binary = list(pixels_hide)
            #error : keep overwriting the last bit
            for dataIndex in range(0,8):
                stegoImage = pixels_cover[:7] + binary[dataIndex]
                dataIndex += 1
        coverImage[x][y] = int(stegoImage, 2)
    cv2.imwrite('StegoImage.png', coverImage)

Then the stego image is read and pass to the extracting function. Here I’m getting the last bit of the selected pixel and form back to pixel. Then those pixel values placed back to the black image. But I’m getting error here as it read extra pixels from the stego image.

def extracting(stgimg,secretImage, password):
    Swidth, Sheight = stgimg.shape
    Ewidth, Eheight = secretImage.shape
    newPixel = []
    pixelList = []
    # create 2 blank images 
    OriImg = np.zeros((Swidth, Sheight, 1), np.uint8) 
    ExtractedImg = np.zeros((Ewidth, Eheight, 1), np.uint8)
    indices = permute_indices(stgimg.shape, password)
    for x in range(Swidth):
        for y in range(Sheight):
            a, b = next(indices)
            stegopixel = format(stgimg[a][b], '08b')
            bitValue = stegopixel[-1]
            newPixel.append(bitValue)
    for i in range(0, len(newPixel),8):
        pixelBit = ''.join(newPixel[i:i+8])
        pixelByte = int(pixelBit, 2)
        pixelList.append(pixelByte)
    img = np.array(pixelList, dtype=np.uint8).reshape(secretImage.shape)
    cv2.imwrite('ExtractedImage.png', ExtractedImg)

Answer

First of all, you have an error in embedding

for dataIndex in range(0,8):
    stegoImage = pixels_cover[:7] + binary[dataIndex]
    dataIndex += 1

You hide 8 bits in the LSB of the same pixel, effectively overwriting them and keeping only the last one. You’re supposed to hide each bit in a different pixel and so you need at least 8 times more pixels in your cover image than what your secret has. With that in mind

def embedding(coverImage, secretImage, password):
    stegoImage = coverImage.copy()
    indices = permute_indices(coverImage.shape, password)
    for pixel in secretImage.flatten():
        # no need for string "bit manipulation", straight up bitwise operations
        for shift in range(7, -1, -1):
            x, y = next(indices)
            bit = (pixel >> shift) & 0x01
            stegoImage[x,y] = (coverImage[x,y] & 0xfe) | bit
    return stegoImage

And then

def extracting(stegoImage, password):
    indices = permute_indices(stegoImage.shape, password)
    pixels = []
    for _ in range(secretImage.size):
        pixel = 0
        for _ in range(8):
            x, y = next(indices)
            bit = stegoImage[x,y] & 0x01
            pixel = (pixel << 1) | bit
        pixels.append(pixel)
    pixels = np.array(pixels, dtype=np.uint8).reshape(secretImage.shape)
    return pixels

Edit: I’ve modified the above functions to return the image arrays instead of saving them, so I can directly test it with the original input. This also assumes that permute_indices is still the same as defined here.

password = 'password'
coverImage = cv2.imread('original.png', 0)
secretImage = makeSecretImage(coverImage)
stegoImage = embedding(coverImage, secretImage, password)
extractedImage = extracting(stegoImage, password)

diff = coverImage.astype(int) - stegoImage
print(diff.min(), diff.max())                 # Differences should be in the [-1, 1] range
print(np.all(secretImage == extractedImage))  # True