Image Steganography avec python opencv, la reconstruction de l'image intégrée est très bruyante
Je cache une image dans une autre image (Image Steganography) en utilisant python 3.6.8 avec opencv 4.4.0.44. Je suis sur une machine Windows 10.
L'algorithme que j'utilise est le suivant: j'ai défini un masque avec des zéros aux deux derniers bits significatifs les plus bas. Ensuite, j'utilise ce masque et "bitwise and" pour rendre les deux derniers bits de chaque pixel de l'image de base à zéro. Il y a deux images, l'une est l'image de base qui contient la deuxième image (image cachée). Je me suis assuré que la taille de l'image cachée était au plus 1/4 de l'image de base. J'ai également changé les deux images en échelle de gris pour ne traiter qu'un seul canal.
J'ai réussi à intégrer l'image et à l'extraire, mais l'image extraite est très bruyante, ce qui me surprend car le contenu de l'image n'a pas changé.
import numpy as np
import cv2 as cv
import os
def mask_n_bit_of_image(img_array, mask):
"""
Applies a mask bitwise on an image to make the n lowest bit zero
:param img: input image
:param mask: mask to make the n lowest significant bits zero. Maske sample: int('11111110', 2)
:return: masked image
"""
for i in range(img_array.shape[0]):
for j in range(img_array.shape[1]):
new_value = img_array[i, j] & mask
img_array[i, j] = new_value
return img_array
def draw_img_side_by_side(img1, img2, caption):
h_im = cv.hconcat([img_cp, img])
cv.imshow(caption, h_im)
def image_binary_content(input_array):
"""
Calculates the binary content of an input numpy array of type int.
:param input_array: input numpy array which is a gray_scale image
:return: binary content of the image in str format
"""
img_cp = []
for x in range(0, input_array.shape[0]):
for y in range(0, input_array.shape[1]):
img_cp.append(bin(int(input_array[x, y]))[2:])
# reshaping the list to match the image size and order
new_img_arr = np.reshape(img_cp, (input_array.shape[0], input_array.shape[1]))
return new_img_arr
def padding_zeros_to_make_8bits_images(input_image):
"""
Checks the output of image_binary_content(img) to add zeros to the left hand side of every byte.
It makes sure every pixel is represented by 8 bytes
:param input_image: input image or numpy 2D array
:return: numpy 2D array of 8-bits pixels in binary format
"""
for i in range(input_image.shape[0]):
for j in range(input_image.shape[1]):
if len(input_image[i, j]) < 8:
# print(input_image[i, j])
zeros_to_pad = 8 - len(input_image[i, j])
# print('Zeros to pad is {}'.format(zeros_to_pad))
elm = input_image[i, j]
for b in range(zeros_to_pad):
elm = '0' + elm
# print('New value is {} '.format(elm))
input_image[i, j] = elm
# print('double check {} '.format(input_image[i, j]))
return input_image
def write_img(path, name, img):
"""
:param path:
:param name:
:param img:
:return:
"""
name = os.path.join(path, name)
cv.imwrite(name, img)
img_path = 's2.bmp'
img = cv.imread(img_path, 0)
cv.imshow('original image', img)
img_cp = img.copy()
path_dest = r'color'
print('Original image shape {}'.format(img.shape))
mask = int('11111100', 2)
print('mask = {}'.format(mask))
img_n2 = mask_n_bit_of_image(img, mask)
# draw_img_side_by_side(img_cp, img_n2, 'Modified image n=2')
img_to_hide_path = r'2.jpeg'
img_to_hide = cv.imread(img_to_hide_path, 0)
img_to_hide = cv.resize(img_to_hide, (220, 220), interpolation=cv.INTER_NEAREST)
# for images which are bigger than 1/4 of the base image, resize them:
# img_to_hide = cv.resize(img_to_hide, (500, 420), interpolation=cv.INTER_NEAREST)
cv.imshow('hidden image', img_to_hide)
h_flat = img_to_hide.flatten()
print('LENGTH OF FLAT HIDDEN IMAGE IS {}'.format(len(h_flat)))
# for i in range(len(h_flat)):
# print(bin(h_flat[i]))
img_hidden_bin = image_binary_content(img_to_hide)
print('binary of hidden image type: {}'.format(type(img_hidden_bin)))
# reformat evey byte of the hidden image to have 8 bits pixels
img_hidden_bin = padding_zeros_to_make_8bits_images(img_hidden_bin)
print(img_hidden_bin.shape)
all_pixels_hidden_img = img_hidden_bin.flatten()
print('Length of flattened hidden image to embed is {}'.format(len(all_pixels_hidden_img)))
# for i in range(0, 48400):
# print(all_pixels_hidden_img[i])
num_pixels_to_modify = len(all_pixels_hidden_img) * 4
print('Number of pixels to modify in base image is {}'.format(num_pixels_to_modify))
# parts = [your_string[i:i+n] for i in range(0, len(your_string), n)]
two_bit_message_list = []
for row in all_pixels_hidden_img:
for i in range(0, 8, 2):
two_bit_message_list.append(row[i: i+2])
print('TWO BITS MESSAGE LIST LENGTH {}'.format(len(two_bit_message_list)))
# reconstruct the hidden msg to make sure for the next part
# c_h_img = []
# for i in range(0, len(two_bit_message_list), 4):
# const_byte = two_bit_message_list[i] + two_bit_message_list[i+1] + two_bit_message_list[i+2] + two_bit_message_list[i+3]
# c_h_img.append(const_byte)
#
# print('constructed image length c_h_img {}'.format(len(c_h_img)))
# for i in range(48400):
# print(c_h_img[i])
# c_h_img = np.array(c_h_img, np.float64)
# c_h_img = c_h_img.reshape(img_to_hide.shape)
# cv.imshow('C_H_IMG', c_h_img.astype('uint16'))
# insert 6 zeros to left hand side of every entry to two_bit_message_list
new_hidden_image = []
for row in two_bit_message_list:
row = '000000' + row
new_hidden_image.append(row)
base_img_flat = img_cp.flatten()
num_bytes_to_fetch = len(two_bit_message_list)
img_base_flat = img_n2.flatten()
print('LENGTH OF TWO BIT MSG LIST {}'.format(num_bytes_to_fetch))
print('Bit length of the bytes to fetch is {} '.format(bin(num_bytes_to_fetch)))
# scanned from new constructed image
print(bin(num_bytes_to_fetch)[2:])
print(len( bin(num_bytes_to_fetch)[2:] ))
print('Start of loop to embed the hidden image in base image')
for i in range(num_bytes_to_fetch):
# First 12 bytes are reserved for the hidden image size to be embedded
new_value = img_base_flat[i] | int( new_hidden_image[i], 2)
img_base_flat[i] = new_value
image_with_hidden_img = img_base_flat.reshape(img_n2.shape)
cv.imshow('Image with hidden image embedded', image_with_hidden_img)
# Reading embedded image from constructed image
constructed_image_with_message_embedded = image_binary_content(image_with_hidden_img)
constructed_image_with_message_embedded_zero_padded = padding_zeros_to_make_8bits_images(constructed_image_with_message_embedded)
flat_constructed_image_with_message_embedded = constructed_image_with_message_embedded_zero_padded.flatten()
embedded_img_list = []
for i in range(num_bytes_to_fetch):
embedded_img_list.append(flat_constructed_image_with_message_embedded[i][-2:])
# [print(rec) for rec in embedded_img_list]
print('EMBEDDED IMAGE LIST LENGTH {}'.format(len(embedded_img_list)))
const_byte_list = []
for i in range(0, len(embedded_img_list), 4):
const_byte = embedded_img_list[i] + embedded_img_list[i+1] + embedded_img_list[i+2] + embedded_img_list[i+3]
const_byte_list.append(const_byte)
# [print(rec) for rec in const_byte_list]
print('LENGTH OF CONSTRUCT BYTES IS {}'.format(len(const_byte_list)))
const_byte_list_tmp = np.array(const_byte_list, np.float64)
const_byte_2D_array = const_byte_list_tmp.reshape(img_to_hide.shape) #((220,220))
const_byte_2D_array = const_byte_2D_array.astype('uint16')
cv.imshow('Constructed image from base', const_byte_2D_array)
cv.imwrite('reconstructed_image.jpeg', const_byte_2D_array)
cv.waitKey(0)
cv.destroyAllWindows()
s2.bmp
2.jpeg

J'ai essayé différentes extensions d'image, y compris jpg, png et bmp. Dans tous, l'image reconstruite était déformée. Dans l'image ci-dessous, vous pouvez voir à quel point l'image reconstruite est bruyante. L'image de la nature est l'image de base contenant l'image cachée dans son lsb, l'œil supérieur est l'image cachée, l'œil inférieur est l'image cachée reconstruite.

Mes propres pensées: comme j'ai ce problème pour différents types d'images, et comme vous le voyez dans mon code, il y a un bloc que j'ai commenté (à partir de la ligne 134 dans github), je pense que la source du problème devrait se trouver à la méthode "image_binary_content ". Si vous décommentez le bloc à la ligne 134, vous obtiendrez exactement la même image reconstruite avant même de l'incorporer dans l'image de base. J'ai fait des comparaisons et je suis presque sûr que le contenu de l'image cachée est correctement récupéré, mais avant d'être intégré, certaines données ont été perdues.
Mon code est le suivant et disponible sur ce github_link sous le nom hw3_task1_embed_image_in_base_image.py
. La base et l'image cachée y sont également disponibles. Vous pouvez également retrouver l'image cachée reconstruite après l'avoir traitée à partir de l'image de base sous le nom "reconstruced_image.png" (par capture d'écran), "reconstruced_image.jpeg" par cv.imwrite. Fait intéressant, ce que j'ai sauvegardé par imwrite a une qualité bien inférieure à ce qui est montré en exécutant le code.
Réponses
Le contenu de const_byte_list
est équivalent à celui de all_pixels_hidden_img
, qui sont les pixels d'image secrets sous forme de chaîne binaire. Votre erreur survient peu de temps après, avec
const_byte_list_tmp = np.array(const_byte_list, np.float64)
Vous pouvez penser que cela convertit la chaîne binaire «11001000» en valeur 200, mais en fait cela la transforme en nombre flottant 11001000.0. Au lieu de cela, vous voulez ce qui suit
const_byte_list_tmp = np.array([int(pixel, 2) for pixel in const_byte_list], dtype=np.uint8)
Remarquez comment le tableau est défini pour taper uint8 et non uint16.
Cela dit, vous vous y prenez dans le mauvais sens. Vous avez utilisé une opération BITAND quelque part, vous connaissez donc les opérations au niveau du bit. Et c'est ainsi que doit se faire la stéganographie, ces opérations agissant sur des entiers. Au fond, 0b11111111, 255 et 0xff sont toutes des représentations du même nombre. Vous n'avez pas besoin de convertir des entiers en chaîne binaire, de les couper et de les assembler, puis de les reconvertir en entiers.
Numpy prend également en charge les opérations vectorisées, donc array & mask
l'appliquera à tous les éléments sans avoir besoin de boucles explicites. Dans l'ensemble, votre code pourrait ressembler à ceci.
MASK_ZERO = 0b11111100
MASK_EXTRACT = 0b00000011
cover_path = 's2.bmp'
secret_path = '2.jpeg'
# EMBED
cover = cv.imread(cover_path, 0)
secret = cv.imread(secret_path, 0)
secret = cv.resize(secret, (220, 220), interpolation=cv.INTER_NEAREST)
secret_bits = []
for pixel in secret.flatten():
secret_bits.extend(((pixel >> 6) & MASK_EXTRACT,
(pixel >> 4) & MASK_EXTRACT,
(pixel >> 2) & MASK_EXTRACT,
pixel & MASK_EXTRACT))
secret_bits = np.array(secret_bits)
secret_length = len(secret_bits)
stego = cover.copy().flatten()
stego[:secret_length] = (stego[:secret_length] & MASK_ZERO) | secret_bits
# EXTRACT
extracted_bits = stego[:secret_length] & MASK_EXTRACT
extracted = []
for i in range(0, secret_length, 4):
extracted.append((extracted_bits[i] << 6) |
(extracted_bits[i+1] << 4) |
(extracted_bits[i+2] << 2) |
extracted_bits[i+3])
extracted = np.array(extracted, dtype=np.uint8)
extracted = extracted.reshape(secret.shape)
print('Is extracted secret correct: {}'.format(np.all(secret == extracted)))