隐藏图原理及程序实现

引言

隐藏图又叫幻影坦克,指的是在QQ、贴吧、知乎等移动端软件上的一种图片,该图片在未打开大图时为一个样子,点开大图时会变成另一幅图。本文会简单介绍该图的原理,并用Python实现。


原理

原理是为了更好的实现,你也可以直奔后面的实现。

大白话版

关于隐藏图的原理,网上会有很多。简单来说就是,一般图片只有RGB颜色信息,而png格式图片还包含透明度的值,一旦图片有了透明度,就会叠加上背景色。而QQ的图片在缩略图时,背景是白色,大图时背景是黑色。一个颜色在具有透明度时,在黑色背景和白色背景下显示的会不一样,利用这一特性,制作合成图。

公式版

基础原理

大白话可能说的不太明白,用公式可能会好一点。

首先要了解,我们在电脑上看到的颜色,都是一个个很密集的点形成的,而一个点的颜色有三个值来控制,即R G B。一般看到的隐藏图都是灰色的,称为灰度图,也就是说R=G=B,三个值相等。

了解了图像的基本原理,再来看刚刚说过的透明度问题。

假设现在又ImgAImgB两张图,将ImgA放在ImgB上面,最终看到的图片叫做ImgH,ImgA不透明度记为O

  • 那么一般来说,只能看到ImgA,看不到ImgB。因为ImgA的不透明度为100%。
  • 假设ImgA的不透明度为0%,那么就只看得到ImgB
  • 如果ImgA的不透明度介于0-100%之间,看到的图片就是两张图的叠加。

现在只考虑R值,其余G B值计算方法相同。用公式表示上述的现象就是:ImgH.R=ImgA.R*O+ImgB.R*(1-O)

进阶原理

了解了上述的基础,那么再来看我们在真实的幻影坦克是怎么形成的。上述代号中,将O的定义从ImgA的不透明度变更为ImgH的不透明度。然后在灰度图中,R=G=B,所以不再单独写图片的通道,用图片本身代替三个的值,即ImgA=ImgA.R

隐藏图在白色背景下显示的是ImgA,黑色背景显示的是ImgB,用公式来就是:

  • 白色背景:ImgA=ImgH*O+255*(1-O),其中255表示白色的R=G=B=255值。
  • 黑色背景:ImgB=ImgH*O+0*(1-O),聪明的你是不是猜到了,0表示黑色的R=G=B=0值。

这个一看,已知ImgAImgB,求ImgHO,是不是很简单。

  • O=255-ImgA+ImgB
  • ImgH=ImgB/O

其中这些运算,都是针对图片的R G B值分别进行运算的。如果计算结果超过255,就取255,如果分母为0的,要变成一个很小的数,例如千万分之一。这是程序实现需要注意的地方!

除此之外呢,还有一个需要注意的地方就是,ImgA应该处理的更亮,而ImgB处理的偏暗,这样才能保证在缩略图的时候,亮的部分在白底下更亮,暗的部分在白底下隐藏。打开为大图时相反。也就是这一点,导致了PS处理的没有程序生成的效果好。

顺便附带一份PS教程


程序实现

根据公式我们一点点来。我读取图片用的库名字叫opencv-python,导入的时候为cv2

  • O=255-ImgA+ImgB
  • ImgH=ImgB/O

    反相

    首先是255-ImgA,熟悉PS的人知道,这一步叫做反相。
    1
    2
    3
    def inversion(pic):
    # pic 类型为numpy.ndarray
    return 255 - pic

线性减淡(添加)

再接下来是两个图片相加Imgx+ImgY,PS中叫做线性减淡(添加):

1
2
3
4
5
6
7
def linear_add(pic1, pic2):
# pic1,pic2 类型为numpy.ndarray,输入的图片
# out 两图片叠加后的结果
out = pic1 + pic2
# 防止相加结果超过255
out[out > 255] = 255
return out

划分

然后是看似除法的东西(其实就是除法),ImgB/O,也就是PS中的划分:

1
2
3
4
5
6
7
8
9
def divide(pic1, pic2):
# pic1,pic2 类型为numpy.ndarray,输入的图片
pic2 = pic2.astype(np.float)
# 防止分母为0
pic2[pic2 == 0] = 1e-10
out = (pic1 / pic2) * 255
# 防止结果超过255
out[out > 255] = 255
return out

取红色通道

然后,取255-ImgA+ImgB的结果作为不透明度:

1
2
3
def get_red_channel(pic):
# 只取了输入图片的R值作为ImgH的不透明度,因为灰度图R=G=B
return pic[:, :, 2]

更改不透明度

给图片添加不透明度:

1
2
3
4
5
6
def add_alpha(pic, A):
# pic 输入图片
# A 透明度
# 图片格式为BGRA,最后一位A是不透明度,255为不透明,0为全透明。
pic[:, :, 3] = A
return pic

彩图变为灰度图

然后就是一些附加操作,先是将图片变为灰度图,并加上不透明度通道:

1
2
3
4
5
6
7
8
9
def rgb2gray(pic):
# pic 输入图片,类型为numpy.ndarray
pic_shape = pic.shape
out = np.ones((pic_shape[0], pic_shape[1], 4)) * 255
temp = cv2.cvtColor(pic, cv2.COLOR_BGR2GRAY)
out[:, :, 0] = temp
out[:, :, 1] = temp
out[:, :, 2] = temp
return out.astype(np.uint8)

改变图片亮度

还有改变图片的亮度,这个映射会导致ImgARGB值普遍大于ImgBRGB值。

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
def change_color_level(pic, is_light):
# pic 要改变的图片
# is_light True变亮,False变暗
# light_table和dark_table来自网络
light_table = [120, 120, 121, 121, 122, 122, 123, 123, 124, 124, 125, 125, 126, 126, 127, 127, 128, 128,
129, 129, 130, 130, 131, 132, 132, 133, 133, 134, 134, 135, 135, 136, 136, 137, 137, 138,
138, 139, 139, 140, 140, 141, 142, 142, 143, 143, 144, 144, 145, 145, 146, 146, 147, 147,
148, 148, 149, 149, 150, 150, 151, 152, 152, 153, 153, 154, 154, 155, 155, 156, 156, 157,
157, 158, 158, 159, 159, 160, 161, 161, 162, 162, 163, 163, 164, 164, 165, 165, 166, 166,
167, 167, 168, 168, 169, 170, 170, 171, 171, 172, 172, 173, 173, 174, 174, 175, 175, 176,
176, 177, 177, 178, 179, 179, 180, 180, 181, 181, 182, 182, 183, 183, 184, 184, 185, 185,
186, 186, 187, 188, 188, 189, 189, 190, 190, 191, 191, 192, 192, 193, 193, 194, 194, 195,
195, 196, 197, 197, 198, 198, 199, 199, 200, 200, 201, 201, 202, 202, 203, 203, 204, 205,
205, 206, 206, 207, 207, 208, 208, 209, 209, 210, 210, 211, 211, 212, 212, 213, 214, 214,
215, 215, 216, 216, 217, 217, 218, 218, 219, 219, 220, 220, 221, 222, 222, 223, 223, 224,
224, 225, 225, 226, 226, 227, 227, 228, 228, 229, 229, 230, 231, 231, 232, 232, 233, 233,
234, 234, 235, 235, 236, 236, 237, 237, 238, 239, 239, 240, 240, 241, 241, 242, 242, 243,
243, 244, 244, 245, 245, 246, 247, 247, 248, 248, 249, 249, 250, 250, 251, 251, 252, 252,
253, 253, 254, 255]
dark_table = [0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10,
10, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21,
22, 22, 23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 28, 28, 29, 29, 30, 30, 31, 32, 32,
33, 33, 34, 34, 35, 35, 36, 36, 37, 37, 38, 38, 39, 39, 40, 41, 41, 42, 42, 43, 43,
44, 44, 45, 45, 46, 46, 47, 47, 48, 48, 49, 50, 50, 51, 51, 52, 52, 53, 53, 54, 54,
55, 55, 56, 56, 57, 57, 58, 59, 59, 60, 60, 61, 61, 62, 62, 63, 63, 64, 64, 65, 65,
66, 66, 67, 68, 68, 69, 69, 70, 70, 71, 71, 72, 72, 73, 73, 74, 74, 75, 75, 76, 77,
77, 78, 78, 79, 79, 80, 80, 81, 81, 82, 82, 83, 83, 84, 85, 85, 86, 86, 87, 87, 88,
88, 89, 89, 90, 90, 91, 91, 92, 92, 93, 94, 94, 95, 95, 96, 96, 97, 97, 98, 98, 99,
99, 100, 100, 101, 102, 102, 103, 103, 104, 104, 105, 105, 106, 106, 107, 107, 108,
108, 109, 109, 110, 111, 111, 112, 112, 113, 113, 114, 114, 115, 115, 116, 116, 117,
117, 118, 119, 119, 120, 120, 121, 121, 122, 122, 123, 123, 124, 124, 125, 125, 126,
127, 127, 128, 128, 129, 129, 130, 130, 131, 131, 132, 132, 133, 133, 134, 135]

pic_shape = pic.shape
out = np.zeros((pic_shape[0], pic_shape[1], 4), dtype=np.uint8)
if is_light:
out[:, :, 0] = [[light_table[y] for y in x] for x in pic[:, :, 0]]
out[:, :, 1] = [[light_table[y] for y in x] for x in pic[:, :, 1]]
out[:, :, 2] = [[light_table[y] for y in x] for x in pic[:, :, 2]]
out[:, :, 3] = pic[:, :, 3]
else:
out[:, :, 0] = [[dark_table[y] for y in x] for x in pic[:, :, 0]]
out[:, :, 1] = [[dark_table[y] for y in x] for x in pic[:, :, 1]]
out[:, :, 2] = [[dark_table[y] for y in x] for x in pic[:, :, 2]]
out[:, :, 3] = pic[:, :, 3]
return out

其中映射的结果来自这篇安卓教程。博主还提供了一个APP,可以去下载直接使用。

完整代码

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
import cv2
import numpy as np


def linear_add(pic1, pic2):
out = pic1 + pic2
out[out > 255] = 255
return out


def divide(pic1, pic2):
pic2 = pic2.astype(np.float)
pic2[pic2 == 0] = 1e-10
out = (pic1 / pic2) * 255
out[out > 255] = 255
return out


def inversion(pic):
return 255 - pic


def rgb2gray(pic):
pic_shape = pic.shape
out = np.ones((pic_shape[0], pic_shape[1], 4)) * 255
temp = cv2.cvtColor(pic, cv2.COLOR_BGR2GRAY)
out[:, :, 0] = temp
out[:, :, 1] = temp
out[:, :, 2] = temp
return out.astype(np.uint8)


def get_red_channel(pic):
return pic[:, :, 2]


def add_alpha(pic, A):
pic[:, :, 3] = A
return pic


def change_color_level(pic, is_light):
light_table = [120, 120, 121, 121, 122, 122, 123, 123, 124, 124, 125, 125, 126, 126, 127, 127, 128, 128,
129, 129, 130, 130, 131, 132, 132, 133, 133, 134, 134, 135, 135, 136, 136, 137, 137, 138,
138, 139, 139, 140, 140, 141, 142, 142, 143, 143, 144, 144, 145, 145, 146, 146, 147, 147,
148, 148, 149, 149, 150, 150, 151, 152, 152, 153, 153, 154, 154, 155, 155, 156, 156, 157,
157, 158, 158, 159, 159, 160, 161, 161, 162, 162, 163, 163, 164, 164, 165, 165, 166, 166,
167, 167, 168, 168, 169, 170, 170, 171, 171, 172, 172, 173, 173, 174, 174, 175, 175, 176,
176, 177, 177, 178, 179, 179, 180, 180, 181, 181, 182, 182, 183, 183, 184, 184, 185, 185,
186, 186, 187, 188, 188, 189, 189, 190, 190, 191, 191, 192, 192, 193, 193, 194, 194, 195,
195, 196, 197, 197, 198, 198, 199, 199, 200, 200, 201, 201, 202, 202, 203, 203, 204, 205,
205, 206, 206, 207, 207, 208, 208, 209, 209, 210, 210, 211, 211, 212, 212, 213, 214, 214,
215, 215, 216, 216, 217, 217, 218, 218, 219, 219, 220, 220, 221, 222, 222, 223, 223, 224,
224, 225, 225, 226, 226, 227, 227, 228, 228, 229, 229, 230, 231, 231, 232, 232, 233, 233,
234, 234, 235, 235, 236, 236, 237, 237, 238, 239, 239, 240, 240, 241, 241, 242, 242, 243,
243, 244, 244, 245, 245, 246, 247, 247, 248, 248, 249, 249, 250, 250, 251, 251, 252, 252,
253, 253, 254, 255]
dark_table = [0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10,
10, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21,
22, 22, 23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 28, 28, 29, 29, 30, 30, 31, 32, 32,
33, 33, 34, 34, 35, 35, 36, 36, 37, 37, 38, 38, 39, 39, 40, 41, 41, 42, 42, 43, 43,
44, 44, 45, 45, 46, 46, 47, 47, 48, 48, 49, 50, 50, 51, 51, 52, 52, 53, 53, 54, 54,
55, 55, 56, 56, 57, 57, 58, 59, 59, 60, 60, 61, 61, 62, 62, 63, 63, 64, 64, 65, 65,
66, 66, 67, 68, 68, 69, 69, 70, 70, 71, 71, 72, 72, 73, 73, 74, 74, 75, 75, 76, 77,
77, 78, 78, 79, 79, 80, 80, 81, 81, 82, 82, 83, 83, 84, 85, 85, 86, 86, 87, 87, 88,
88, 89, 89, 90, 90, 91, 91, 92, 92, 93, 94, 94, 95, 95, 96, 96, 97, 97, 98, 98, 99,
99, 100, 100, 101, 102, 102, 103, 103, 104, 104, 105, 105, 106, 106, 107, 107, 108,
108, 109, 109, 110, 111, 111, 112, 112, 113, 113, 114, 114, 115, 115, 116, 116, 117,
117, 118, 119, 119, 120, 120, 121, 121, 122, 122, 123, 123, 124, 124, 125, 125, 126,
127, 127, 128, 128, 129, 129, 130, 130, 131, 131, 132, 132, 133, 133, 134, 135]

pic_shape = pic.shape
out = np.zeros((pic_shape[0], pic_shape[1], 4), dtype=np.uint8)
if is_light:
out[:, :, 0] = [[light_table[y] for y in x] for x in pic[:, :, 0]]
out[:, :, 1] = [[light_table[y] for y in x] for x in pic[:, :, 1]]
out[:, :, 2] = [[light_table[y] for y in x] for x in pic[:, :, 2]]
out[:, :, 3] = pic[:, :, 3]
else:
out[:, :, 0] = [[dark_table[y] for y in x] for x in pic[:, :, 0]]
out[:, :, 1] = [[dark_table[y] for y in x] for x in pic[:, :, 1]]
out[:, :, 2] = [[dark_table[y] for y in x] for x in pic[:, :, 2]]
out[:, :, 3] = pic[:, :, 3]
return out


def make(file1, file2):
surface_pic = cv2.imread(file1)
hidden_pic = cv2.imread(file2)
sur_shape = surface_pic.shape
hid_shape = hidden_pic.shape
out_shape = (min(sur_shape[0], hid_shape[0]), min(sur_shape[1], hid_shape[1]))
surface_pic = cv2.resize(surface_pic, out_shape)
hidden_pic = cv2.resize(hidden_pic, out_shape)

surface_pic = rgb2gray(surface_pic)
# cv2.imshow('test', surface_pic)
# cv2.waitKey(0)
surface_pic = change_color_level(surface_pic, True)
# cv2.imshow('test', surface_pic)
# cv2.waitKey(0)
surface_pic = inversion(surface_pic)
# cv2.imshow('test', surface_pic)
# cv2.waitKey(0)

hidden_pic = rgb2gray(hidden_pic)
# cv2.imshow('test', hidden_pic)
# cv2.waitKey(0)
hidden_pic = change_color_level(hidden_pic, False)
# cv2.imshow('test', hidden_pic)
# cv2.waitKey(0)

out_pic = linear_add(surface_pic, hidden_pic)
# cv2.imshow('test', out_pic)
# cv2.waitKey(0)
A = get_red_channel(out_pic)
out_pic = divide(hidden_pic, out_pic)
out_pic = add_alpha(out_pic, A)
return out_pic


if __name__ == '__main__':
f1 = '22.jpg'
f2 = '11.jpg'
pic = make(f1, f2)
cv2.imwrite(r'1.png', pic)

后记

参考了很多,原理都是png带有透明度,但网上还有别的实现方法,是通过将两个图片以网格的形式交叉填充,形成最终的图片,类似见教程1教程2。但是这种效果都没有公式推导的方式效果好。


参考


TODO

根据参考中的幻影坦克架构指南(三),接下来期望能实现彩色的隐藏图,即在白色背景下为轻微彩色的灰度图,黑色背景下为较暗但是质量很高的彩色图。