使用 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)))
我通过使用 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)))