教你造一台幻影坦克

@2024年4月20日 1.8k字 §技术 #图像处理
目录
  • 透明度混合
  • 色彩解算
  • 小瑕疵
  • 提高幻影坦克质量
  • 亮度调整算法
  • 小工具
  • 幻影坦克相信大家都见过吧,它是命令与征服系列游戏中的一种盟军高科单位 它是一种特殊的图片,在一般情况下看起来没有异常,但是一旦置于特定的背景颜色下就会变成另一张图片。

    因为其表现形式和红警中的幻影坦克很像,于是得名“幻影坦克”。

    其正式称呼为“双层图”,利用 PNG 等带有透明度图层的图片,通过在不同的背景色下突显不同的像素而制作出来。双层图也是一种常见的图片隐写技术,在 CTF 等比赛中也有一席之地。

    透明度混合

    前面说到幻影坦克利用的是带有透明度图层的图片,实际上应用的是数字图像处理中常见的 透明度混合 算法。

    透明度混合算法其实非常简单,假设一个带有透明度的颜色 SS 需要混合到背景颜色 BB 上,该颜色的透明度为 aa,那么混合后的颜色 DD 为:

    D=Sa+B(1a)D=Sa+B(1-a)

    假设现在我们需要将 红色 以 30% 不透明度混合到

    蓝色 背景上,那么有:

    S=[255,0,0]B=[0,0,255]D=Sa+B(1a)=[255,0,0]0.3+[0,0,255](10.3)=[127.5,0,0]+[0,0,178.5]=[127.5,0,178.5]=[127,0,178]=7f00b2\begin{aligned} S &= [255, 0, 0]\\ B &= [0, 0, 255]\\ D &= Sa+B(1-a)\\ &= [255, 0, 0] \cdot 0.3 + [0, 0, 255] \cdot (1 - 0.3)\\ &= [127.5, 0, 0] + [0, 0, 178.5]\\ &= [127.5, 0, 178.5]\\ &= [127, 0, 178]\\ &= \mathrm{\textcolor{#7f00b2}{7f00b2}} \end{aligned}

    混合得到的色彩是

    偏蓝的紫色,当然如果你将混合不透明度提高到 70%,那么有:

    D=Sa+B(1a)=[255,0,0]0.7+[0,0,255](10.7)=[178.5,0,0]+[0,0,127.5]=[178.5,0,127.5]=[178,0,127]=7f00b2\begin{aligned} D &= Sa+B(1-a)\\ &= [255, 0, 0] \cdot 0.7 + [0, 0, 255] \cdot (1 - 0.7)\\ &= [178.5, 0, 0] + [0, 0, 127.5]\\ &= [178.5, 0, 127.5]\\ &= [178, 0, 127]\\ &= \mathrm{\textcolor{#b2007f}{7f00b2}} \end{aligned}

    混合得到的色彩是 偏红的紫色

    色彩解算

    我们知道通过让透明色叠加在其他颜色上可以得到新的颜色,那么制作幻影坦克的过程则相反,我们需要通过已知的两种图以及两种背景,反推出最终的幻影坦克。

    对于在一般情况下看见的图片我们称为 前景图,而隐藏其中的图则是 背景图,对应的生效背景颜色也就顺理成章地叫做 前景色背景色
    假设前景图的颜色为 S1S_1,背景图的颜色为 S2S_2,前景色和背景色分别为 B1B_1B2B_2
    我们需要解算的幻影坦克颜色为 DD,不透明度为 aa,那么不难得到方程组:

    S1=Da+B1(1a)S2=Da+B2(1a)\begin{aligned} S_1 &= Da + B_1(1 - a)\\ S_2 &= Da + B_2(1 - a)\\ \end{aligned}

    DaDa 可以换元得到一元一次方程:

    S1=Da+B1(1a)Da=S1B1(1a)\begin{aligned} & S_1 = Da + B_1(1 - a)\\ \rArr& Da = S_1 - B_1(1 - a)\\ \end{aligned} S2=Da+B2(1a)S2=S1B1(1a)+B2(1a)S2S1=(B2B1)(1a)\begin{aligned} & S_2 = Da + B_2(1 - a)\\ \rArr& S_2 = S_1 - B_1(1 - a) + B_2(1 - a)\\ \rArr& S_2 - S_1 = (B_2 - B_1)(1 - a)\\ \end{aligned}

    由于 B1B_1B2B_2 不相等(相等了哪来的幻影坦克),所以进一步地有:

    S2S1=(B2B1)(1a)1a=S2S1B2B1a=1S2S1B2B1a=1S1S2B1B2\begin{aligned} & S_2 - S_1 = (B_2 - B_1)(1 - a)\\ \rArr& 1 - a = \frac{S_2 - S_1}{B_2 - B_1}\\ \rArr& a = 1 - \frac{S_2 - S_1}{B_2 - B_1}\\ \rArr& a = 1 - \frac{S_1 - S_2}{B_1 - B_2}\\ \end{aligned}

    aa 不为 00 时,对于颜色 DD 有:

    Da=S1B1(1a)Da=S1B1+B1aD=S1B1a+B1\begin{aligned} & Da = S_1 - B_1(1 - a)\\ \rArr& Da = S_1 - B_1 + B_1a\\ \rArr& D = \frac{S_1 - B_1}{a} + B_1\\ \end{aligned}

    aa00 时,表示幻影坦克当前像素不透明度为 00(该像素完全透明),那么此时 DD 为任意数值都可以。

    在解算得到 DDaa 之后,由于常用图片格式规定了像素通道值为 0 ~ 255 整数,而解算的结果可能会超出该范围,因此需要使用钳制函数和取整函数进行处理,而使用了钳制函数和取整函数就意味着图像信息丢失,这往往是造成幻影坦克效果不佳的原因。

    小瑕疵

    在上一节中所示的计算公式有一个小瑕疵,那就是 它不能用来处理多通道图片,只有单通道图片(灰度)才能使用。

    如果考虑 RGB 多通道,那么整个方程组会变得复杂:

    RS1=RDa+RB1(1a)GS1=GDa+GB1(1a)BS1=BDa+BB1(1a)RS2=RDa+RB2(1a)GS2=GDa+GB2(1a)BS2=BDa+BB2(1a)\begin{aligned} R_{S1} &= R_Da + R_{B1}(1 - a)\\ G_{S1} &= G_Da + G_{B1}(1 - a)\\ B_{S1} &= B_Da + B_{B1}(1 - a)\\ R_{S2} &= R_Da + R_{B2}(1 - a)\\ G_{S2} &= G_Da + G_{B2}(1 - a)\\ B_{S2} &= B_Da + B_{B2}(1 - a)\\ \end{aligned}

    整合之后有:

    RS1RS2=(RB1RB2)(1a)GS1GS2=(GB1GB2)(1a)BS1BS2=(BB1BB2)(1a)\begin{aligned} R_{S1} - R_{S2} &= (R_{B1} - R_{B2})(1 - a)\\ G_{S1} - G_{S2} &= (G_{B1} - G_{B2})(1 - a)\\ B_{S1} - B_{S2} &= (B_{B1} - B_{B2})(1 - a)\\ \end{aligned}

    此时要使得 aa 有解,图片和色彩的限制将变得非常苛刻,所以一般情况下我们都不会考虑将两张彩色图合成幻影坦克(当然如果你有兴趣根据约束条件调整像素的话请努力试试)。

    提高幻影坦克质量

    在幻影坦克制造过程中,通常使用纯白或纯黑作为前后景色,因此我们的色彩变换式子可以进一步简化:

    a=1S1S2255D=S1255a+255\begin{aligned} a &= 1 - \frac{S_1 - S_2}{255}\\ D &= \frac{S_1 - 255}{a} + 255\\ \end{aligned}

    通过对取值范围的计算不难知道 aa 会落在 0 ~ 2 之间,DD 可能是小于 255 的任何一个数,因此如果不对原图进行处理,很有可能会导致色彩解算被钳制而丢失信息。

    首先 aa 的取值范围 1 ~ 2 会被钳制,逆推可得当 S1S_1 小于 S2S_2 时会发生,因此为了防止信息丢失,我们需要保证大部分的 S1S_1 亮度应该高于 S2S_2,那么显而易见的处理思路就是整体增亮 S1S_1 并减暗 S2S_2,这也是为什么很多幻影坦克看起来就跟过曝而点进去又黑乎乎一片的原因。

    其次对 DD 的范围进行不等式限制后得到关系式:

    255(1a)S1255255(1-a) \leq S_1 \leq 255

    可以看出要使 S1S_1 能够不受钳制影响,那么它的可选范围只有 255a255a,并且变化的只有下限,因此同样可以通过提高 S1S_1 的亮度来保证其不被钳制。

    亮度调整算法

    对灰度图像进行亮度调整有两种主要方法:直接偏移值调整对数变换

    直接偏移值调整即通过直接对灰度加上或减去一个固定值来调整亮度,当然调整之后的数值会经过钳制,对于暗部或亮部细节就会丢失。直接偏移调整是最常用的算法。

    对数变换则通过一个对数函数映射每一个灰度值,它可以做到在尽可能保留图像细节的情况下提升亮度。设输入灰度为 SS,输出灰度为 DD,增益因子为 cc,对数的底为 ee,则有:

    D=cloge(1+S)D=c\log_e(1+S)

    当然我们也可以将以上两种方法结合起来使用以获得想要的效果。

    小工具

    以下小工具简单实现了幻影坦克工厂,前两个图为经过预处理后的原图,后两个图分别为在白色背景和黑色背景下的幻影坦克效果图。

    注意

    使用的浏览器前端图片处理工具是同步代码,所以在生成的时候可能会导致页面卡顿,请不要以为是浏览器卡死了

    前景图选择

    背景图选择

    图片缩放(1)

    前景图灰度增益 (0)

    背景图灰度减益 (0)

    使用对数变换

    前景图对数增益因子 (1)

    前景图对数增益底数 (2)

    背景图对数减益因子 (2)

    背景图对数减益底数(1)

    正在加载索引……