使用 python opencv 的图像隐写术,重建嵌入的图像非常嘈杂

Image Steganography with python opencv, reconstructing the embedded image is very noisy

我通过使用 python 3.6.8 和 opencv 4.4.0.44 将图像隐藏在另一个图像中(图像隐写术)。我在 windows 10 台机器上。

我正在使用的算法如下:我定义了一个掩码,在最后两个最低有效位处带有零。然后我使用这个掩码和“按位与”它使基础图像中每个像素的最后两位为零。有两个图像,一个是容纳第二个图像(隐藏图像)的基础图像。我已确保隐藏图像的大小最多为基本图像的 1/4。我还更改了两张灰度图像以仅处理一个通道。

我已经成功地嵌入和提取了图像,但是提取的图像非常嘈杂,这让我很惊讶,因为图像的内容没有改变。

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

我尝试过不同的图像扩展名,包括 jpg、png 和 bmp。在所有这些图像中,重建的图像都被扭曲了。在下图中,您可以看到重建图像的噪声有多大。自然图像是在其lsb中包含隐藏图像的基础图像,上眼是隐藏图像,下眼是重建的隐藏图像。

我自己的想法: 当我遇到不同图像类型的问题时,正如您在我的代码中看到的那样,我评论了一个块(从第 134 行开始 github),我认为问题的根源应该在于方法“image_binary_content”。如果您取消注释第 134 行的块,您将获得完全相同的重建图像,甚至在将其嵌入基础图像之前。我做了比较,我很确定隐藏图像的内容被正确检索,但在嵌入之前一些数据丢失了。

我的代码如下,可以在 github_link 上找到,名称为 hw3_task1_embed_image_in_base_image.py。基本图像和隐藏图像也在那里可用。您还可以在名称“reconstructed_image.png”(通过屏幕截图)、“reconstructed_image.jpeg”通过名称“cv.imwrite”下找到从基础图像处理后重建的隐藏图像。有趣的是,我通过 imwrite 保存的质量比 运行 代码显示的质量低得多。

const_byte_list中的内容等同于all_pixels_hidden_img中的内容,都是二进制字符串形式的秘密图像像素。您的错误很快就会出现,

const_byte_list_tmp = np.array(const_byte_list, np.float64)

您可能认为这会将二进制字符串“11001000”转换为值 200,但实际上它会将其转换为浮点数 11001000.0。相反,您想要以下内容

const_byte_list_tmp = np.array([int(pixel, 2) for pixel in const_byte_list], dtype=np.uint8)

注意数组是如何设置为类型 uint8 而不是 uint16。


说了这么多,你的做法是错误的。您在某处使用过 BITAND 运算,因此您了解按位运算。这就是隐写术应该如何完成的,这些操作作用于整数。在深处 0b11111111、255 和 0xff 都是同一个数字的表示。您不必将整数转换为二进制字符串,切割和拼接它们,然后将它们转回整数。

Numpy 还支持矢量化操作,因此 array & mask 会将其应用于所有元素,无需显式循环。总而言之,您的代码可能如下所示。

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