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:
Please help me out with this.
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)