Featured image of post 利用透明度通道制作 QQ 内表里不一的图片

利用透明度通道制作 QQ 内表里不一的图片

今天在 QQ 看到一张图,这张图在预览下看到的和查看图片时看到的内容不一样。虽然不是第一次见这种图了,但是今天在摸鱼的时候无事可做,就研究了一下它的原理。

这张图是这样的:左图为它在聊天界面的样子,典型的吃桃场景,点开大图之后看到的却是右图的样子(笑)。 中间的图片为嵌入的原图,如果您开启了暗色模式(桌面端可以在页面左下角切换),您看到中图的应该和右图一样,反之则和左图一样。 原图可以在这里获取。

原理

这种图的原理并不难猜,肯定利用了 png 图片的透明度通道,使其在不同背景色下能呈现出不同的效果。下面求解一下它是怎么制作出来的: (因为这里只涉及图像的像素变换,为简化表示,以下的每个变量都表示单个像素单个通道的值,且定义域为$[0,1]$.)

设在聊天界面看到的图像(表图)灰度为$c$,点开大图后看到的图像(里图)为$h$,待求量为生成的图像的灰度$g$与透明度$a$。

在聊天界面的图像是目标图像和白色背景的混合,也就是$$(1-a) + g\cdot a = c.$$ 点开大图后是目标图像和黑色背景的混合,即$$0+g\cdot a = h.$$

解出来就是$$a=1+h-c,~g=h/a.$$

又因为解需要满足$a,g\in(0,1]$,所以$c$和$h$需要满足不等式$$h\le c<1+h$$ 右侧当且仅当$c=1,~h=0$时不成立。

实现

这个程序实现起来没有什么难度,python 代码如下(需要 Pillow 与 numpy)。需要注意的是,为了满足上面的不等式,在程序中需要重新分配两张原图的灰度的范围。

 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
import numpy as np
from PIL import Image

# 读取图片+转灰度
cover = Image.open("./cover.png").convert("L")
hidden = Image.open("./hidden.png").convert("L")
assert cover.size == hidden.size
cover = np.asarray(cover)
hidden = np.asarray(hidden)

# 按照约束条件调整像素范围
cover_min, cover_max = cover.min(), cover.max()
hidden_min, hidden_max = hidden.min(), hidden.max()
cd = cover_max - cover_min
hd = hidden_max - hidden_min
divide_portion = hd/(hd*1.0+cd)
cover_f = (cover-cover_min)*1.0/cd * (1-divide_portion-1/255) + divide_portion
hidden_f = (hidden-hidden_min)*1.0/hd * divide_portion

# 计算
alpha = 1-cover_f+hidden_f
gray = hidden_f/alpha
alpha = (alpha*255).astype(np.uint8)
gray = (gray*255).astype(np.uint8)
result = np.stack([gray,gray,gray,alpha], axis=-1)

# 保存结果
img = Image.fromarray(result)
img.save("./output.png")

这是随手抓的两张黑白表情包做出来的效果,顺序和上面的展示一样,您可以通过切换暗色模式查看效果:

注:在 QQ 发送此类图片时,一定要选择发送原图,否则 QQ 可能会在压缩过程中去掉透明度通道,导致其失效。

本文基于CC BY-NC-SA 4.0 协议发布。本文不可商用,转载时请注明出处并使用相同协议
使用 Hugo 构建
主题 StackJimmy 设计