EDIT 24.03.2017: I decided to refuse form JPEG and YCBCR. I'm using bmp image and RGB, however the problem is still there.
I'm trying to make Zhao-Koch's steganography algorithm realization, however the extracted message does not correspond to the impemented and I can't seem to grasp, what causes it.
Here's the code:
Implementation:
from PIL import Image
from sklearn.feature_extraction import image
import numpy as np
from scipy.fftpack import dct
from scipy.fftpack import idct
pic = Image.open('lama.bmp') # container, 400x400 bmp picture
pic_size = pic.size #picture size
(r, g, b) = pic.split() #splitting the colour channels
u1 = 4 # coordinates for the DCT coefficients to change. [u1][v1] and [u2][v2]
v1 = 5
u2 = 5
v2 = 4
P = 25 # Threshold value to compare the difference of the coefficients with
cvz = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1] # test message
i = 0 #
acb = np.asarray(b, dtype='int64') # colour channel as array. int64 because absolute difference may go out of the [0,255] boundaries.
patches = image.extract_patches_2d(acb, (8, 8)) # dividing array to 8x8 blocks
for patch in patches: # Applying
dct(patch, overwrite_x = True)
while (i < len(cvz)): # going through message bits
patch = patches[i] # take block
K1 = patch[u1][v1] # first coefficient
K2 = patch[u2][v2] # second coefficient
K = abs(K1) - abs(K2) # difference of absolute values
cur_bit = cvz[i] # take one bit of the message
if (cur_bit == 1) & (K >= -P): # Implementation works the following way: if message bit is 0 than K must be more than P. If it's 1, K must be less than -P. If the requirements are not met, the coefficients change.
i = i +1
while (K >= -P): # changing coefficient
K1 = K1 - 1
print(K1)
K2 = K2 + 1
print(K2)
K = abs(K1) - abs(K2)
patch[u1][v1] = K1 # applying new values
patch[u2][v2] = K2 # applying new values
elif (cur_bit == 0) & (K <= P): # changing coefficient
i = i + 1
while (K <= P):
K1 = K1 + 1
print(K1)
K2 = K2 - 1
print(K2)
K = abs(K1) - abs(K2)
patch[u1][v1] = K1 # applying new values
patch[u2][v2] = K2 # applying new values
else: # requirements are met and there is no need to change coefficients
i = i + 1
for patch in patches: # applying IDCT to blocks
idct(patch, overwrite_x = True)
acb2 = image.reconstruct_from_patches_2d(patches, (400,400)) # reconstructing colour channel
acb2 = acb2.astype(np.uint8) # converting
b_n = Image.fromarray(acb2, 'L') # converting colour channel array to image
changed_image = Image.merge('RGB', (r,g,b_n)) # merging channels to create new image
changed_image.save("stego.bmp") # saving image
Extraction:
from PIL import Image
from sklearn.feature_extraction import image
import numpy as np
from scipy.fftpack import dct
from scipy.fftpack import idct
pic = Image.open('stego.bmp')
(r, g, b) = pic.split()
u1 = 4
v1 = 5
u2 = 5
v2 = 4
length = 13
i = 0
cvz = []
acb = np.asarray(b, dtype='int64')
patches = image.extract_patches_2d(acb, (8, 8))
for patch in patches:
dct(patch,overwrite_x = True)
while (i < length): # extracting message. If absolute of coefficient 1 is more than absolute of coefficient 2 than message bit is 0. Otherwise it's 1
patch = patches[i]
print (patch[u1][v1])
print (patch[u2][v2])
K1 = abs(patch[u1][v1])
K2 = abs(patch[u2][v2])
if (K1 > K2):
cvz.append(0)
i = i + 1
else:
cvz.append(1)
i = i + 1
print(cvz)
However the extracted message is wrong:
Original message:
[1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1]
Extracted message:
[1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1]
I'm guessing that I'm doing something wrong with coefficient changes.
Can someone help me with it, please?
UPD: It seems that changed DCT coefficients are not saved since I can't find them in changed picture if I try to look for them specifically.
Your code has several issues, namely the 8x8 blocks overlap, the DCT is applied to only one dimension of the image, the way coefficients are changed (K1 = K1 - 1
and K2 = K2 + 1
) does not guarantee that the threshold condition is met, etc. To fix all those problems I came up with the following implementation:
Import the necessary modules, set up the parameters and define some useful functions
import numpy as np
from skimage import io
from skimage.util import view_as_blocks
from scipy.fftpack import dct, idct
u1, v1 = 4, 5
u2, v2 = 5, 4
n = 8
P = 25
def double_to_byte(arr):
return np.uint8(np.round(np.clip(arr, 0, 255), 0))
def increment_abs(x):
return x + 1 if x >= 0 else x - 1
def decrement_abs(x):
if np.abs(x) <= 1:
return 0
else:
return x - 1 if x >= 0 else x + 1
Functions to change the DCT coefficients
def abs_diff_coefs(transform):
return abs(transform[u1, v1]) - abs(transform[u2, v2])
def valid_coefficients(transform, bit, threshold):
difference = abs_diff_coefs(transform)
if (bit == 0) and (difference > threshold):
return True
elif (bit == 1) and (difference < -threshold):
return True
else:
return False
def change_coefficients(transform, bit):
coefs = transform.copy()
if bit == 0:
coefs[u1, v1] = increment_abs(coefs[u1, v1])
coefs[u2, v2] = decrement_abs(coefs[u2, v2])
elif bit == 1:
coefs[u1, v1] = decrement_abs(coefs[u1, v1])
coefs[u2, v2] = increment_abs(coefs[u2, v2])
return coefs
Inserting a message into an image
def embed_bit(block, bit):
patch = block.copy()
coefs = dct(dct(patch, axis=0), axis=1)
while not valid_coefficients(coefs, bit, P) or (bit != retrieve_bit(patch)):
coefs = change_coefficients(coefs, bit)
print coefs[u1, v1], coefs[u2, v2]
patch = double_to_byte(idct(idct(coefs, axis=0), axis=1)/(2*n)**2)
return patch
def embed_message(orig, msg):
changed = orig.copy()
blue = changed[:, :, 2]
blocks = view_as_blocks(blue, block_shape=(n, n))
h = blocks.shape[1]
for index, bit in enumerate(msg):
print 'index=%d, bit=%d' % (index, bit)
i = index // h
j = index % h
block = blocks[i, j]
blue[i*n: (i+1)*n, j*n: (j+1)*n] = embed_bit(block, bit)
changed[:, :, 2] = blue
return changed
Extracting the hidden message
def retrieve_bit(block):
transform = dct(dct(block, axis=0), axis=1)
return 0 if abs_diff_coefs(transform) > 0 else 1
def retrieve_message(img, length):
blocks = view_as_blocks(img[:, :, 2], block_shape=(n, n))
h = blocks.shape[1]
return [retrieve_bit(blocks[index//h, index%h]) for index in range(length)]
In [291]: original = io.imread('https://i.sstatic.net/TUV0V.png')
In [292]: test_message = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1]
In [293]: changed = embed_message(original, test_message)
In [294]: retrieve_message(changed, len(test_message))
Out[294]: [1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1]
In [295]: io.imshow(np.hstack((original, changed)))
Out[295]: <matplotlib.image.AxesImage at 0x106c7c18>
Results: original image (left) and image with hidden message (right)
In [296]: np.random.seed(0)
In [297]: long_message = np.random.randint(0, 2, 300)
In [298]: long_message
Out[298]:
array([0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1,
0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1,
1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1,
0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0,
0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0,
0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0,
0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0,
1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1,
0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1,
1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1,
0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0,
1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1,
1])
In [299]: changed2 = embed_message(original, long_message)
In [300]: np.all(long_message == retrieve_message(changed2, len(long_message)))
Out[300]: True