图片只在你的浏览器里处理,不会上传服务器。 隐私说明
算法入门

一条曲线如何把图片变成噪点:Gilbert 曲线入门

图片混淆的核心不是“加噪声”,而是“换顺序”。要让换顺序之后的画面足够乱,又能在反向操作时精确还原,需要一条覆盖每个像素、又不留下明显规律的路径。

如果把一张图的所有像素看成一排格子,重排就是把这些格子换个顺序。难的不是“换”,而是“换得够乱、还能换回来、并且不依赖随机数”。

为什么不能简单地按行重排

最直观的做法是逐行扫描:第一行从左到右,第二行从右到左,像蛇形一样走完整张图。这样一来,每个像素都有了线性顺序,重排也能做。

但问题很明显:相邻的像素仍然相邻。原本在同一行里的颜色,重排后还是排在一起,只是顺序变了。人眼对连续的色块非常敏感,即使顺序打乱,轮廓和纹理仍然可能被辨认出来。整体平移几行,也只是把画面错位,内容依然清楚。

要让人眼难以还原,遍历顺序必须在二维空间里“跳得够远”,同时又不能随机。随机顺序当然也能打散画面,但接收方需要知道同一个随机种子,否则无法还原;种子一旦泄露,规则就失效。我们需要一条确定的、可重复的、覆盖所有像素的路径。

空间填充曲线是什么

空间填充曲线是一条连续的线,它在一个区域里不断转弯,最终经过每一个格子。可以把它想象成一只蚂蚁在方格纸上走,每一步只走到相邻的格子,最后把所有格子都走一遍,而且不重复。

常见的空间填充曲线有 Peano 曲线、Hilbert 曲线、Z-order 曲线(Morton 曲线)等。它们的共同点是:把二维空间映射成一维顺序,同时尽量保留“邻近性”——在二维里靠近的点,在一维顺序里也往往不会离得太远。

邻近性对图片压缩、数据库索引很有用;对图片混淆来说,它反而是一个需要被“打散”的性质。所以我们先利用曲线得到一个稳定的顺序,再用一个整体偏移,把邻近性破坏掉。

Hilbert 与 Gilbert 的区别

Hilbert 曲线通常要求区域是 2 的幂次方大小的正方形,比如 2×2、4×4、8×8、16×16。图片很少刚好满足这个条件;为了用它,通常需要把图片补成正方形,处理完再裁掉。补边会引入额外的像素,也会改变尺寸,对一个要求“原尺寸可逆”的工具来说并不理想。

Gilbert 曲线是 Hilbert 曲线的推广,可以直接处理任意宽高的矩形。它不需要补边,也不需要把图片裁成正方形。对于一张 1920×1080 的截图或 4032×3024 的照片,这一点很实际:不用为了算法去改图片尺寸。

本站的实现参考了广义 Hilbert 曲线的递归写法:根据当前矩形的长宽比,决定是“长边切成两段”,还是“短边切成三块”,然后对每一块递归处理。递归到只剩一行或一列时,直接按顺序填充。

递归:大问题拆成小矩形

可以把整个过程想象成切蛋糕。给定一个矩形,先看它更像“长条”还是“方块”:

  • 如果宽度远大于高度,就把长边切成两段,分别处理,再把两段的路径接起来;
  • 如果高度相对更大,就把短边切成三块,按特定的方向依次处理,让路径在块与块之间自然衔接;
  • 如果只剩一行或一列,就直接沿着那个方向走完。

递归的终止条件是一维(一行或一列),这时候不需要再切。每一层都只做“切分”和“拼接”,不修改像素,只记录顺序。最终得到的是一串长度等于像素总数的下标。

下面是一个简化后的伪代码,省略了方向处理,只保留结构:

function generate(x, y, ax, ay, bx, by):
    w = |ax + ay|
    h = |bx + by|

    if h == 1:
        fill row
        return
    if w == 1:
        fill column
        return

    split the rectangle into smaller pieces
    generate(piece 1)
    generate(piece 2)
    generate(piece 3)   # 某些情况下只有两块

实际实现里还要处理方向、奇偶步长和坐标计算。本站把这些细节放在 assets/js/algorithm.js 的 gilbert2d 函数里,不依赖 DOM,方便单独验证。

曲线有了,为什么还要偏移

如果只按曲线顺序重新写一遍,画面不会变成噪点。曲线只是给出了一个“座位表”,真正把画面打散的是整体偏移:

offset = round(((√5 − 1) / 2) × W × H)

编码时,原来在曲线第 i 位的像素,被搬到第 i + offset 位;解码时反向搬回。0.618 这个比例让相邻像素被拆到画面中相隔很远的位置,同时又不需要随机种子。偏移量只依赖宽高,因此同一张图每次处理的结果一致。

这也解释了一个常见疑问:为什么换一张尺寸相同的图,混淆后的“噪点风格”看起来差不多?因为曲线和偏移量只由尺寸决定,颜色只是跟着原来的像素走。画面看起来都是噪点,但每个点的颜色来自不同的原位置。

为什么一定能还原

“整体平移 offset 位”是一个置换:每个位置恰好被取走一次,也恰好被写入一次。没有两个像素落到同一个位置,也没有位置被漏掉。置换都有逆操作,把同样的步骤反过来做一遍,就回到了原始顺序。

这里有两个前提:

  1. 尺寸一致。曲线和偏移量都由宽高决定,尺寸变了,规则就变了;
  2. 像素值没有被改。有损压缩、缩放、裁剪都会改变像素值或位置,导致还原出现色差或错位。

只要这两个前提成立,还原就是确定的,不需要原图,也不需要密码。

它不是加密,也不该假装是

Gilbert 曲线和偏移量都是公开的。任何人拿到混淆图和本站页面,都能还原。它没有密钥,也不提供保密性。它的价值在于“降低随手可见”,而不是“阻止有目的的查看”。

如果你需要真正的保密,应该使用带密钥的加密工具。混淆和加密解决的是不同的问题,不能互相替代。理解这一点,比记住曲线名字更重要。

想亲手看看效果?

打开 算法原理页的交互演示,拖动偏移量滑块,观察一张 16 × 16 的小图如何从清晰变成噪点。演示只改变位置,不改变颜色,和真实工具的逻辑一致。

相关阅读