引言
隐藏图又叫幻影坦克,指的是在QQ、贴吧、知乎等移动端软件上的一种图片,该图片在未打开大图时为一个样子,点开大图时会变成另一幅图。本文会简单介绍该图的原理,并用Python实现。
原理
原理是为了更好的实现,你也可以直奔后面的实现。
大白话版
关于隐藏图的原理,网上会有很多。简单来说就是,一般图片只有RGB颜色信息,而png
格式图片还包含透明度的值,一旦图片有了透明度,就会叠加上背景色。而QQ的图片在缩略图时,背景是白色,大图时背景是黑色。一个颜色在具有透明度时,在黑色背景和白色背景下显示的会不一样,利用这一特性,制作合成图。
公式版
基础原理
大白话可能说的不太明白,用公式可能会好一点。
首先要了解,我们在电脑上看到的颜色,都是一个个很密集的点形成的,而一个点的颜色有三个值来控制,即R G B
。一般看到的隐藏图都是灰色的,称为灰度图,也就是说R=G=B
,三个值相等。
了解了图像的基本原理,再来看刚刚说过的透明度问题。
假设现在又ImgA
和ImgB
两张图,将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
值。
这个一看,已知ImgA
和ImgB
,求ImgH
和O
,是不是很简单。
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
3def inversion(pic):
# pic 类型为numpy.ndarray
return 255 - pic
线性减淡(添加)
再接下来是两个图片相加Imgx+ImgY
,PS中叫做线性减淡(添加):1
2
3
4
5
6
7def 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
9def 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
3def get_red_channel(pic):
# 只取了输入图片的R值作为ImgH的不透明度,因为灰度图R=G=B
return pic[:, :, 2]
更改不透明度
给图片添加不透明度:1
2
3
4
5
6def add_alpha(pic, A):
# pic 输入图片
# A 透明度
# 图片格式为BGRA,最后一位A是不透明度,255为不透明,0为全透明。
pic[:, :, 3] = A
return pic
彩图变为灰度图
然后就是一些附加操作,先是将图片变为灰度图,并加上不透明度通道:1
2
3
4
5
6
7
8
9def 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)
改变图片亮度
还有改变图片的亮度,这个映射会导致ImgA
的RGB
值普遍大于ImgB
的RGB
值。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
46def 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 | import cv2 |
后记
参考了很多,原理都是png
带有透明度,但网上还有别的实现方法,是通过将两个图片以网格的形式交叉填充,形成最终的图片,类似见教程1,教程2。但是这种效果都没有公式推导的方式效果好。
参考
- ps中图层混合模式算法公式
- Android实现 制作隐藏图片效果 (幻影坦克)
- 精简而又超详细讲解“幻影坦克”图片效果的制作方法
- 一个简单的QQ隐藏图生成算法
- 这种像是点阵图的是如何做到放大后是另一张图片的?
- python实现“幻影坦克”效果(点开图片是隐藏的另一张图)【详解】
- ps图层混合模式之划分模式
- 一篇文章彻底搞清PS混合模式的原理
- 幻影坦克架构指南(三)
TODO
根据参考中的幻影坦克架构指南(三),接下来期望能实现彩色的隐藏图,即在白色背景下为轻微彩色的灰度图,黑色背景下为较暗但是质量很高的彩色图。