pythonimagesteganographydct

Issue Related To DCT Based Steganography


I am trying to perform DCT Steganography by converting the image to HSV and applying DCT and quantizing it and hiding the text message and stiching the image. In decoding process, applying DCT and quantizing it and extracting the text message from it. But, here I am getting incorrect answer in it. I am using HSV for getting the same image color similar to original image. I used saturation channel to hide the text in it. Now, I am stuck in it and not getting the correct answer. Please help me out in this. Here is the code:

# -*- coding: utf-8 -*-
"""
Created on Tue Oct 19 11:13:50 2021 
@author: SM
"""
  
from PIL import Image
import numpy as np
import itertools
import types
import cv2
from Crypto.Cipher import AES
#creation of quantization matrix of quality factor as 50
quant = np.array([[16,11,10,16,24,40,51,61],
                [12,12,14,19,26,58,60,55],
                [14,13,16,24,40,57,69,56],
                [14,17,22,29,51,87,80,62],
                [18,22,37,56,68,109,103,77],
                [24,35,55,64,81,104,113,92],
                [49,64,78,87,103,121,120,101],
                [72,92,95,98,112,100,103,99]])
class DiscreteCosineTransform:
    #created the constructor
    def __init__(self):
        self.message = None
        self.bitMessage = None
        self.oriCol = 0
        self.oriRow = 0
        self.numBits = 0
    #utility and helper function for DCT Based Steganography
    #helper function to stich the image back together
    def chunks(self,l,n):
        m = int(n)
        for i in range(0,len(l),m):
            yield l[i:i+m]
    #function to add padding to make the function dividable by 8x8 blocks
    def addPadd(self,img,row,col):
         img = cv2.resize(img,(col+(8-col%8),row+(8-row%8)))
         return img
    #function to transform the message that is wanted to be hidden from plaintext to a list of bits
    def toBits(self):
         bits = []
         for char in self.message:
            binval = bin(char)[2:].rjust(8,'0')
            #print('bin '+binval)
            bits.append(binval)
         self.numBits = bin(len(bits))[2:].rjust(8,'0')
         return bits
    #main part 
    #encoding function 
    #applying dct for encoding 
    def DCTEncoder(self,img,secret):
        self.message = str(len(secret)).encode()+b'*'+secret
        self.bitMessage = self.toBits()
        #get the size of the image in pixels
        row, col = img.shape[:2]
        self.oriRow = row
        self.oriCol = col
        if((col/8)*(row/8)<len(secret)):
            print("Error: Message too large to encode in image")
            return False
        if(row%8!=0 or col%8!=0):
            img = self.addPadd(img,row,col)
        row,col = img.shape[:2]
        #split image into RGB channels
        hImg,sImg,vImg = cv2.split(img)
        #message to be hid in blue channel so converted to type float32 for dct function
        #print(bImg.shape)
        sImg = np.float32(sImg)
        #breaking the image into 8x8 blocks
        imgBlocks = [np.round(sImg[j:j+8,i:i+8]-128) for (j,i) in itertools.product(range(0,row,8),range(0,col,8))]
        #print(imgBlocks[0])
        #blocks are run through dct / apply dct to it
        dctBlocks = [np.round(cv2.dct(ib)) for ib in imgBlocks]
        #print('DCT Blocks')
        #print(dctBlocks[0])
        #blocks are run through quantization table / obtaining quantized dct coefficients
        quantDCT = [np.round(dbk/quant) for dbk in dctBlocks]
        #print('Quant Blocks')
        #print(quantDCT[0])
        #set LSB in DC value corresponding bit of message
        messIndex=0
        letterIndex=0
        print(self.bitMessage)
        for qb in quantDCT:
            #find LSB in DCT cofficient and replace it with message bit
            #print(len(qb))
            DC = qb[0][0]
            #print(DC.shape)
            DC = np.uint8(DC)
            #print(DC)
            DC = np.unpackbits(DC)
            #print(DC[0])
            #print(self.bitMessage[messIndex][letterIndex])
            #print(DC[7])
            #print(type(DC[7]))
            #print(DC[7].shape)
            #print(type(self.bitMessage))
            #a=self.bitMessage[messIndex][letterIndex]
            #print(a)
            DC[7] = self.bitMessage[messIndex][letterIndex]
            DC = np.packbits(DC)
            DC = np.float32(DC)
            DC = DC - 255
            qb[0][0] = DC
            letterIndex = letterIndex + 1
            if (letterIndex == 8):
                letterIndex = 0
                messIndex = messIndex + 1
                if (messIndex == len(self.message)):
                    break
        #writing the stereo image
        #blocks run inversely through quantization table
        sImgBlocks = [quantizedBlock *quant+128 for quantizedBlock in quantDCT]
        #blocks run through inverse DCT
        #sImgBlocks = [cv2.idct(B)+128 for B in quantizedDCT]
        #puts the new image back together
        aImg=[]
        for chunkRowBlocks in self.chunks(sImgBlocks, col/8):
            for rowBlockNum in range(8):
                for block in chunkRowBlocks:
                    aImg.extend(block[rowBlockNum])
        print(len(aImg))
        aImg = np.array(aImg).reshape(row, col)
        #converted from type float32
        aImg = np.uint8(aImg)
        #show(sImg)
        aImg = cv2.merge((hImg,aImg,vImg))
        return aImg
    #decoding
    #apply dct for decoding 
    def DCTDecoder(self,img):
        row, col = img.shape[:2]
        messSize = None
        messageBits = []
        buff = 0
        #split the image into RGB channels
        hImg,sImg,vImg = cv2.split(img)
        #message hid in blue channel so converted to type float32 for dct function
        sImg = np.float32(sImg)
        #break into 8x8 blocks
        imgBlocks = [sImg[j:j+8,i:i+8]-128 for (j,i) in itertools.product(range(0,row,8),range(0,col,8))]
        #dctBlocks = [np.round(cv2.dct(ib)) for ib in imgBlocks]
        # the blocks are run through quantization table
        quantDCT = [ib/quant for ib in imgBlocks]
        i=0
        flag = 0
        nb = ''
        #message is extracted from LSB of DCT coefficients
        for qb in quantDCT:
            DC = qb[0][0]
            DC = np.uint8(DC)
            #unpacking of bits of DCT
            DC = np.unpackbits(DC)
            #print('DC',DC,end=' ')
            if (flag == 0):
                if (DC[7] == 1):
                    buff+=(0 & 1) << (7-i)
                elif (DC[7] == 0):
                    buff+=(1&1) << (7-i)
            else:
                if (DC[7] == 1):
                    nb+='0'
                elif (DC[7] == 0):
                    nb+='1'
            i=1+i
            #print(i)
            if (i == 8):
                #print(buff,end=' ')
                if (flag == 0):
                    messageBits.append(buff)
                    #print(buff,end=' ')
                    buff = 0
                else:
                    messageBits.append(nb)
                    #print(nb,end=' ')
                    nb = ''
                i =0
                if (messageBits[-1] == 42 and messSize is None):
                    try:
                        flag = 1
                        messSize = int(str(chr(messageBits[0]))+str(chr(messageBits[1])))#int(''.join(messageBits[:-1]))
                        print(messSize,'a')
                    except:
                        print('b')
                        pass
            if (len(messageBits) - len(str(messSize)) - 1 == messSize):
                #print(''.join(messageBits)[len(str(messSize))+1:])
                return messageBits
                pass
        print(messageBits)
        return ''
def msg_encrypt(msg,cipher):
    if (len(msg)%16 != 0):
        #a = len(msg)%16 != 0 
        #print(a)
        msg = msg + ' '*(16 - len(msg)%16)
        #nonce = cipher.nonce
    t1 = msg.encode()
    enc_msg = cipher.encrypt(t1)
    return enc_msg
def msg_decrypt(ctext,cipher):
    dec_msg = cipher.decrypt(ctext)
    msg1 = dec_msg.decode()
    return msg1
image = cv2.imread('C://Users//hp//Desktop//Lenna.jpg',cv2.IMREAD_UNCHANGED)
image = cv2.cvtColor(image,cv2.COLOR_BGR2HSV_FULL)
#image = cv2.cvtColor(image,cv2.COLOR_RGB2HSV)
secret_msg = 'Shaina'
print(secret_msg)
key = b'Sixteen byte key'
#encryption of message
cipher = AES.new(key,AES.MODE_ECB)
enc_msg = msg_encrypt(secret_msg,cipher)
print(enc_msg)
d = DiscreteCosineTransform() 
dct_img_encoded = d.DCTEncoder(image, enc_msg) 
dct_img_encoded = cv2.cvtColor(dct_img_encoded,cv2.COLOR_HSV2BGR_FULL)
#dct_img_encoded = cv2.cvtColor(dct_img_encoded,cv2.COLOR_BGR2RGB)
cv2.imwrite('C://Users//hp//Desktop//DCT1.png',dct_img_encoded)
eimg = cv2.imread('C://Users//hp//Desktop//DCT1.png',cv2.IMREAD_UNCHANGED)
eimg = cv2.cvtColor(eimg,cv2.COLOR_BGR2HSV_FULL)
#eimg = cv2.cvtColor(eimg,cv2.COLOR_RGB2HSV)
text = d.DCTDecoder(eimg)
ntext = []
print(text)
for i in range(len(text)):
    if(type(text[i]) == str):
        ntext.append(text[i])
print(ntext)
#print(type(text))
#print(next)
#binary_data = ''.join([ format(ord(i), "08b") for i in next ])
#all_bytes = [ binary_data[i: i+8] for i in range(0,len(binary_data),8)]
decoded_data = b''
for byte in next:
    try:
        decoded_data += int (byte,2).to_bytes (len(byte) // 8, byteorder='big')
    except Exception as e:
        print(byte)
        break
print(decoded_data)
#decryption of message
dtext = msg_decrypt(decoded_data,cipher)
print(dtext)

The result I am getting it as:
enter image description here

Please help me out with this.


Solution

  • OK, I've simplified a lot of things and made some changes, and this seems to work with my sample images.

    The biggest overall problem you seem to be facing is that the RGB/HSV conversion screws up your least significant bits, thereby losing the embedded message. I'm not convinced manipulating the saturation is the right method. What I've done here is left it in RGB, and I'm manipulating the green band. I'm not doing the quantizing, because I don't think that's a correct method, but I am embedding the message in the bottom 5 bits of the 0th DCT element. That way, I can do some rounding during the decode to allow for a few bits of slop.

    Maybe this can help you move forward.

    from PIL import Image
    import numpy as np
    import itertools
    import cv2
    
    class DiscreteCosineTransform:
        #created the constructor
        def __init__(self):
            self.message = None
            self.numBits = 0
    
        #utility and helper function for DCT Based Steganography
        #helper function to stich the image back together
        def chunks(self,l,n):
            m = int(n)
            for i in range(0,len(l),m):
                yield l[i:i+m]
        #function to add padding to make the function dividable by 8x8 blocks
        def addPadd(self,img,row,col):
             img = cv2.resize(img,(col+(8-col%8),row+(8-row%8)))
             return img
    
        #main part 
        #encoding function 
        #applying dct for encoding 
        def DCTEncoder(self,img,secret):
            self.message = str(len(secret)).encode()+b'*'+secret
            #get the size of the image in pixels
            row, col = img.shape[:2]
            if((col/8)*(row/8)<len(secret)):
                print("Error: Message too large to encode in image")
                return False
            if row%8 or col%8:
                img = self.addPadd(img,row,col)
            row,col = img.shape[:2]
            #split image into RGB channels
            hImg,sImg,vImg = cv2.split(img)
            #message to be hid in saturation channel so converted to type float32 for dct function
            #print(bImg.shape)
            sImg = np.float32(sImg)
            #breaking the image into 8x8 blocks
            imgBlocks = [np.round(sImg[j:j+8,i:i+8]-128) for (j,i) in itertools.product(range(0,row,8),range(0,col,8))]
            #print('imgBlocks',imgBlocks[0])
            #blocks are run through dct / apply dct to it
            dctBlocks = [np.round(cv2.dct(ib)) for ib in imgBlocks]
            print('imgBlocks', imgBlocks[0])
            print('dctBlocks', dctBlocks[0])
            #blocks are run through quantization table / obtaining quantized dct coefficients
            quantDCT = dctBlocks
            print('quantDCT', quantDCT[0])
            #set LSB in DC value corresponding bit of message
            messIndex=0
            letterIndex=0
            print(self.message)
            for qb in quantDCT:
                #find LSB in DCT cofficient and replace it with message bit
                bit = (self.message[messIndex] >> (7-letterIndex)) & 1
                DC = qb[0][0]
                DC = (int(DC) & ~31) | (bit * 15)
                qb[0][0] = np.float32(DC)
                letterIndex += 1
                if letterIndex == 8:
                    letterIndex = 0
                    messIndex += 1
                    if messIndex == len(self.message):
                        break
            #writing the stereo image
            #blocks run inversely through quantization table
            #blocks run through inverse DCT
            sImgBlocks = [cv2.idct(B)+128 for B in quantDCT]
            #puts the new image back together
            aImg=[]
            for chunkRowBlocks in self.chunks(sImgBlocks, col/8):
                for rowBlockNum in range(8):
                    for block in chunkRowBlocks:
                        aImg.extend(block[rowBlockNum])
            aImg = np.array(aImg).reshape(row, col)
            #converted from type float32
            aImg = np.uint8(aImg)
            #show(sImg)
            return cv2.merge((hImg,aImg,vImg))
    
        #decoding
        #apply dct for decoding 
        def DCTDecoder(self,img):
            row, col = img.shape[:2]
            messSize = None
            messageBits = []
            buff = 0
            #split the image into RGB channels
            hImg,sImg,vImg = cv2.split(img)
            #message hid in saturation channel so converted to type float32 for dct function
            sImg = np.float32(sImg)
            #break into 8x8 blocks
            imgBlocks = [sImg[j:j+8,i:i+8]-128 for (j,i) in itertools.product(range(0,row,8),range(0,col,8))]
            dctBlocks = [np.round(cv2.dct(ib)) for ib in imgBlocks]
            # the blocks are run through quantization table
            print('imgBlocks',imgBlocks[0])
            print('dctBlocks',dctBlocks[0])
            quantDCT = dctBlocks
            i=0
            flag = 0
            #message is extracted from LSB of DCT coefficients
            for qb in quantDCT:
                if qb[0][0] > 0:
                    DC = int((qb[0][0]+7)/16) & 1
                else:
                    DC = int((qb[0][0]-7)/16) & 1
                #unpacking of bits of DCT
                buff += DC << (7-i)
                i += 1
                #print(i)
                if i == 8:
                    messageBits.append(buff)
                    #print(buff,end=' ')
                    buff = 0
                    i =0
                    if messageBits[-1] == 42 and not messSize:
                        try:
                            messSize = int(chr(messageBits[0])+chr(messageBits[1]))
                            print(messSize,'a')
                        except:
                            print('b')
                if len(messageBits) - len(str(messSize)) - 1 == messSize:
                    return messageBits
            print("msgbits", messageBits)
            return None
    
    image = cv2.imread('20210827_092821.jpg',cv2.IMREAD_UNCHANGED)
    
    enc_msg = b'Shaina Sixteen byte key'
    #print(enc_msg)
    
    d = DiscreteCosineTransform() 
    dct_img_encoded = d.DCTEncoder(image, enc_msg) 
    
    cv2.imwrite('2021_encoded.png',dct_img_encoded)
    eimg = cv2.imread('2021_encoded.png',cv2.IMREAD_UNCHANGED)
    
    text = d.DCTDecoder(eimg)
    print(text)
    
    decoded = bytes(text[3:])
    print(decoded)