教你造一台光棱坦克

@2025年11月29日 1.9k字 §技术 #图像处理
目录
  • 图像直方图
  • 色阶控制
  • 棋盘交错拼图
  • 小工具
  • 幻影坦克已经满足不了你了?接下来到达战场的是光棱坦克!

    与幻影坦克不同,光棱坦克 是命令与征服系列游戏中的一种盟军高科单位 是一种需要依赖于特定图片调整算法才能看到底图的藏图方法,通常会使用 曝光度调整亮度调整 来显影。

    为了让你理解光棱坦克的实现原理,接下来我们会通过基础概念循序渐进地教你光棱坦克的制造细节。

    本文中所有的交互小工具需要依赖一些特殊浏览器 API(WorkerOffscreenCanvas),如果无法使用,请考虑更换浏览器

    图像直方图

    与幻影坦克依赖透明度魔法不同,光棱坦克依赖图片的亮度魔法。为了观察一张图片的亮度信息,通常会使用直方图

    以下是帮你计算一张输入图片直方图的小工具,你可以添加图片试试看。

    直方图是一张二维图,横坐标是 像素值,根据图片的位深有着不同的范围(8 位深为 256 格,16 位深为 65536 格)
    纵坐标是一个统计量,即在 整个图片中有多少个像素与横坐标像素值一样,白色的柱越高,那么对应像素的数量越多。

    // img: number[][]
    // w: number
    // h: number
    const getHistogram = (img, w, h) => {
      const yOut = Array(256).fill(0);
    
      for (let col = 0; col < w; col++) {
        for (let row = 0; row < h; row++) {
          const grey = img[w][h];
          yOut[grey]++;
        }
      }
    
      return yOut;
    };

    从直方图中我们同样还可以获得整张图片的信息 —— 如果白色柱大多集中于左边,则图片整体更暗(暗色像素数量多),反之更亮。

    需要注意的是,直方图如果不特别说明 通道,则默认是将图片转换为灰度后再进行统计计算,以此来体现亮度信息,如果指定了通道,那么直方图反应的就是对应色彩通道的像素分布信息了。

    色阶控制

    在通过直方图获取图片的亮度分布信息之后,我们就可以通过 色阶 来对修改图片的像素亮度分布了。

    在专业的图片编辑软件中,色阶的参数可以变得很多很复杂,但是对于制造光棱坦克,我们只需要调整 4 个参数:

    • 输入黑场 bIb_I
    • 输入白场 wIw_I
    • 输出黑场 bOb_O
    • 输出白场 wOw_O

    黑场和白场

    在图片编辑软件的色阶控制中,你能看到一个拖动条,两边分别有一个黑块和白块,控制着像素的亮度范围,这被分别称为 黑场白场


    对于输入,黑场和白场做的是 钳制 + 压缩

    Vout={0if VinbI255VinbIwIbIif bI<Vin<wI255otherwiseV_{out} = \begin{cases} 0 & \text{if } V_{in}\leq b_I\\ 255\cdot\frac{V_{in}-b_I}{w_I-b_I} & \text{if } b_I<V_{in}<w_I\\ 255 & \text{otherwise} \end{cases}

    简单地来说,就是将比黑场还要暗的像素置为纯黑,比白场还要亮的像素置为纯白,而黑场和白场之间的像素重新缩放到纯黑和纯白之间。

    我们此处默认 bI<wIb_I < w_I,且图片是 8 位深,共 256 级

    尝试一下对黑场和白场的调整会对图片和直方图产生什么影响:

    从直方图的比较可以看到,小于黑场的像素被全部压缩到了 0 值,大于白场的像素被全部压缩到了 255,而剩下中间夹着的像素则被均匀拉伸到整个 0 到 255 的范围内。
    图片除了动态范围被压缩以外,还损失了暗部和亮部细节。


    对于输出,黑场和白场做的是 重映射

    Vout=bO+Vin255(wObO)V_{out} = b_O + \frac{V_{in}}{255} \cdot (w_O - b_O)

    简单地来说,就是将原来 0 到 255 范围内的像素值重新映射到 bOb_OwOw_O 范围。

    尝试一下对黑场和白场的调整会对图片和直方图产生什么影响:

    从直方图的比较可以看到,整个图片最终的像素直方图被压缩到了黑场和白场之间的区域,图片的动态范围被收窄了,但是相关的细节有所保留。

    组合变换

    色阶依次组合了输入/输出的黑场/白场变换,最终可以产生如下的变换式:

    Vout={bOif VinbIbO+(VinbI)(wObOwIbI)if bI<Vin<wIwOotherwiseV_{out} = \begin{cases} b_O & \text{if } V_{in}\leq b_I\\ b_O + (V_{in}-b_I)\cdot(\frac{w_O - b_O}{w_I-b_I}) & \text{if } b_I<V_{in}<w_I\\ w_O & \text{otherwise} \end{cases}

    经过组合变换得到的就是我们所需要的色阶参数控制结果:

    光棱藏图

    光棱坦克图片意在通过提高曝光度或抬高亮度来显示隐藏图,它们虽然在视觉效果上相似,但是在数学和物理模型上并不相同。

    亮度

    亮度操作是一种 非物理 的简单偏移操作,用于直接操作图片的整体明暗感觉,一般有三种类型:

    加法模型

    直接对每个像素加一个偏移量 Δ\Delta,然后钳制数值范围:

    Vout=clamp(Vin+Δ,0,255)V_{out}=\text{clamp}(V_{in}+\Delta,0,255)

    这种简单的计算容易造成高光溢出和细节损失。

    仿射模型

    通过乘法和偏移量在调整亮度,在图像编辑软件中常用(即“亮度/对比度”调节):

    Vout=αVin+βV_{out}=\alpha\cdot V_{in}+\beta

    α\alpha 用于控制对比度,β\beta 用于调整亮度,这种方法相比加法可以减少细节损失。

    HSV/HSL 色彩空间调整 V/L 通道

    利用这两个色彩空间中天然包含的亮度通道来进行调整:

    Vout=clamp(Vin+Δ,0,1)V_{out}=\text{clamp}(V_{in}+\Delta,0,1)

    这种变换保留了色彩的色相和饱和度信息,同时更加符合人眼的感知,但是需要经过色彩空间变换,产生计算复杂度。

    曝光度

    曝光度调整模拟了真实相机的感光过程,因此这是一种 物理 操作,它是一个线性+指数缩放过程:

    Vout=Vin2EV_{out}=V_{in}\cdot2^{E}

    需要注意的是,如果图片不是 HDR 或 RAW 直出,那么色彩空间往往是经过 Gamma 调整的非线性空间,而曝光度调整需要在线性空间中,因此这类图片需要先进行逆 Gamma 变换之后,再调整曝光度。

    基于亮度/曝光度算法的藏图

    以上两种方案对于直方图来说,可以大致看作 把白场调低,让亮部被压缩成纯白,把暗部拉伸填充范围。

    因此,对于表图,我们需要 提高黑场 来给里图创造像素空间;对于里图,我们需要将它们压缩到暗部,即 降低白场,这样,当曝光度/亮度抬高时,大部分的表图像素会被压缩到纯白,使得暗部的里图被凸显出来。

    那么我们应该怎么融合这两张图呢?

    棋盘交错拼图

    光棱坦克不像幻影坦克利用多余的透明度通道藏匿信息,而是使用 国际象棋棋盘 排列,交错放置表图和里图的像素。

    这样一来,两张图首先可以保持位置重叠,同样又均分了像素信息,保留了足够的分辨率,一台光棱坦克就这样诞生了。

    小工具

    表图色阶调整
    里图色阶调整
    正在加载索引……