最近玩了一个很有意思的操作,直接把数据存放在图片中,这种算法有很多种,今天就说一种加密算法。

原理

首先需要知道像素,有一张三通道的彩色图片,也就是一个 三个维度的矩阵,每个元素的数字的范围是 0~255,很简单,是一个 8 位二进制的数字,总而言言之,每个像素是一个8位的数字。

三个数值可以直接构成一个像素颜色,也就是RGB(Opencv是使用的BGR,无影响)。那就请看下面的图:

image

你能看出左右红色有什么区别吗?反正我是看不出来,实际上在红色分量上左边是255、右边是254,仅仅差了1,也就是说相差1基本看不出来变化。

那么把八位的数据拆开,最后一位是0是1都不会影响颜色太大变化,因为仅仅相差1。

所以算法就出来了,首先,把一张图片的所有像素的最后一位变成0,这样肉眼是观查不出问题的。然后我们把需要加密的信息重新拆成二进制形式,补充到最后一位上,还是不会看出来(反正我是看不出来)。

程序

我也写出来了这部分程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import cv2
import numpy as np

class ImageData:
def __init__(self, path: str):

# 读取图片
self.imageRead(path)

def __makeHead(self, length: int) -> list:
"""
制作头部数据
:param length: 数据长度
:return:
"""
# 数据长度的字节数量,默认成4字节,我感觉足够了
data_max_size = 4
# 头数据长度,默认32字节
head_size = 32

# 两次判断能否放下数据
if length + head_size > (1 << (8 * data_max_size)):
raise ValueError
if length + head_size > self.size[0] * self.size[1] * self.size[2]:
raise ValueError

# 转换成列表
bin_str = list(bin(length)[2:])

# 校验标志
bin_num0 = [0, 1, 0, 0, 1, 1, 0, 1]

# 校验标志
bin_num1 = [ord(num_str) - 48 for num_str in bin_str]

# 数据长度的补充
bin_num2 = [0 for tem in range(8 * data_max_size - len(bin_num1))]

bin_num3 = [0 for tem in range(8 * 27)]

head = bin_num0 + bin_num2 + bin_num1 + bin_num3

return head

def __binToNumber(self, datas: np.ndarray) -> list:
"""
二进制矩阵转换成十进制列表
:param datas: 二进制矩阵
:return:
"""

# 读取出数据
data = []
for tem in datas:
num = 0
for tem2 in tem:
num = num << 1
num += tem2
data.append(num)

return data


def saveData(self, numbers: list) -> None:
"""
将数据藏在图片中
:param numbers: 数据列表
:return:
"""
# 计算长度
length = len(numbers)

# 设定数据头部
head = self.__makeHead(length)

new_numbers = []

# 数据离散
for number in numbers:
# 转换成列表
bin_list = [(number >> i) & 1 for i in range(8)][::-1]
new_numbers += bin_list

new_numbers = head + new_numbers


numbers_1 = np.array(new_numbers, dtype='uint8')
numbers_2 = np.zeros(self.size[0] * self.size[1] * self.size[2] - numbers_1.shape[0], dtype='uint8')

data = np.concatenate((numbers_1, numbers_2)).reshape(self.size)

# 擦除旧数据
self.cleanData()
self.image += data

def loadData(self) -> tuple:
"""
读取藏在图片中的数据
:return:
"""
# 获得初始的矩阵
data_matrix: np.ndarray = self.image & 1

# 展平
data_matrix = data_matrix.reshape(-1)

# 大小
max_length = data_matrix.shape[0]

# 去掉无效的部分获得的的长度
max_length = max_length - (max_length % 8)
data_matrix = data_matrix[:max_length]

# 8个位一组
data_matrix = data_matrix.reshape((-1, 8))

head_matrix = data_matrix[0:32]

# 读取出头部信息
head = self.__binToNumber(head_matrix)

# 提取出数据总长度
length = head[4] + (head[3] << 8) + (head[2] << 16) + (head[1] << 24)

data_matrix = data_matrix[32:32 + length]

# 读取出数据
data = self.__binToNumber(data_matrix)

return head, data


def cleanData(self) -> None:
"""
清楚图片中的数据
:return:
"""
self.image = self.image & 0xfe

def imageRead(self, path: str) -> None:
"""
读取图片
:param path: 读取图片的路径
:return:
"""

# 读取图片
image = cv2.imread(path)

self.size = image.shape

# 图片转换RGB
self.image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

def imageSave(self, path: str) -> None:
"""
保存图片
:param path: 保存图片的路径
:return:
"""
image = cv2.cvtColor(self.image, cv2.COLOR_RGB2BGR)
cv2.imwrite(path, image)

def imageShow(self) -> None:
"""
显示图片
:return:
"""

cv2.imshow("image", self.image)

cv2.waitKey(0)

cv2.destroyAllWindows()


if __name__ == "__main__":
image = ImageData("pycharm.png")

image.saveData([178, 255, 4, 2, 1, 0])

head, data = image.loadData()

image.imageSave("a.png")

print(data)



程序有点长,我简单说一下作用。

首先我封装成了一个类ImageData,构造函数只有一个参数,就是图片的路径,可以直接加载本地图片。

加载进图片之后,图片会处于ImageData实例化的对象中,也可以再使用imageRead方法重新加载图片,也可以使用imageSave方法保存该对象中的图片。我也写了一个显示的方法imageShow,直接使用可以直接查看。

接下来是关于数据的方法:

  • cleanData方法用于清除图片中的数据,也就是直接将像素中的8位二进制数字的最后一位直接变成0。
  • saveData方法是将一个列表中的数字藏进图片中,但数字只能是8位二进制数字,有特殊需求可以直接去修改。
  • loadData方法会返回图片中的数字,会返回两个值,第一个是相关信息,第二个才是隐藏的数据。