0

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.

Tonechas
  • 13,398
  • 16
  • 46
  • 80
Caiphas Kain
  • 119
  • 2
  • 10
  • 1
    I'm not familiar with this algorithm (or sklearn or scipy), but I _suspect_ that you are losing info in the JPEG compression process (specifically, in the [quantization](https://en.wikipedia.org/wiki/JPEG#Quantization) step). What happens if you save the modified data to a non-lossy format like PNG? – PM 2Ring Mar 23 '17 at 08:25
  • Yes, I tried that. Tried working with straight RGB (changed B instead of Cb). The result was:[1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0] Also tried saving as PNG : [1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1] – Caiphas Kain Mar 23 '17 at 08:31
  • In that case, there _is_ some loss occurring due to JPEG compression / decompression, otherwise there would be no difference when using PNG. But there must be (at least) one other problem elsewhere in the algorithm. Eventually, you _might_ be able to use JPEG, by avoiding storing stegano data in the high frequency components, but I suggest that for the present you use PNG and get that working properly first. – PM 2Ring Mar 23 '17 at 08:55
  • 1
    Check that you aren't losing data due to Numpy overflow / precision loss, like what happened [here](http://stackoverflow.com/questions/42943875/power-operator-on-numpy-array-returns-a-strange-result-is-it-a-bug). – PM 2Ring Mar 23 '17 at 08:55
  • 1
    Read [this](https://stackoverflow.com/questions/35396977/lsb-dct-based-image-steganography). If you're trying to do JPEG steganography, you're doing it wrong. If you do intend to take the IDCT and then save the image, you can't use a JPEG because you will cause another layer of quantisation from the encoding. – Reti43 Mar 23 '17 at 11:51
  • 1
    If you're answering your own question, don't edit that in the question. Instead, rollback this to what the question was and add a separate answer. If you're still having an issue after switching to BMP, be clear on that. – Reti43 Mar 24 '17 at 14:33

1 Answers1

1

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)]

Demo

In [291]: original = io.imread('https://i.stack.imgur.com/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 (left) and changed (right)
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
Tonechas
  • 13,398
  • 16
  • 46
  • 80