图片混淆听起来玄,拆开之后只有三个动作:把二维图片摊成一维序列,按一条固定曲线重新编号,再整体平移一个偏移量。没有随机数,没有密钥,也没有隐藏参数。
先把图片摊平成一串像素
浏览器里的图片最终会变成一块像素缓冲区。每个像素占四个字节,依次是红、绿、蓝和透明度,简称 RGBA。一张宽 W、高 H 的图片,就有 W × H 个像素,也就是 W × H × 4 个字节。
平时我们说“第 x 行第 y 列”,是二维坐标。算法更喜欢一维下标:从左上角开始,按行依次编号,第 y * W + x 个像素就是坐标 (x, y)。这一步只是换了一种记法,没有改动任何颜色。
线性下标 = y × 宽度 + x
字节位置 = 线性下标 × 4
把二维摊成一维之后,问题就变成了:怎样重新排列这一串像素,让画面看起来完全散掉,同时又能无损地排回去?
Gilbert 曲线:给每个像素一个座位
最直接的重排是“整体平移一行”或者“按固定步长跳着取”。这类做法的问题是会留下明显的条纹:原本相邻的像素仍然挨在一起,只是换了个方向,人眼还是能辨认出轮廓。
更好的办法是让遍历顺序在二维空间里不断转弯、填满整个矩形。空间填充曲线就是干这个的:它用一条连续曲线走遍每一个格子,而且尽量照顾相邻关系。常见的 Hilbert 曲线大家听得比较多,本站用的是它的推广形式——Gilbert 曲线。
两者思路相近,区别在于 Gilbert 曲线可以直接处理任意宽高的矩形,不需要先把图片补成正方形。对图片工具来说,这一点很实际:不用为了算法去改动原图尺寸,也就少了一次缩放。
曲线走完之后,每个像素都有了一个“第几位”的编号。这个编号只由宽高决定,同一张图每次生成的顺序完全一样。它是算法的一部分,不是随机种子。
黄金分割偏移:把座位整体转一下
只有曲线还不够。如果编码时直接按曲线顺序原样写回去,画面和原图几乎没有区别。真正让画面散开的,是接下来这个偏移量:
offset = round(((√5 − 1) / 2) × W × H)
(√5 − 1) / 2 约等于 0.618,也就是黄金分割的倒数。把它乘以像素总数再四舍五入,得到一个介于 0 和总数之间的整数。然后做一次整体平移:原来在曲线第 i 位的像素,搬到第 i + offset 位;超过末尾就绕回开头。
为什么偏偏是这个比例?因为它能把相邻的像素拆得足够远,又不需要额外的随机数或密钥。0.618 是一个无理数的近似值,乘以不同的总数会落在不同的位置上,画面不会出现明显的周期性条纹。
要注意:偏移量依赖 W × H。宽高变了,offset 就变了,曲线顺序也变了。这也是为什么图片一旦被缩放或裁剪,就无法用原来的规则还原。
拖动滑块,看画面如何散开
下面是一张 16 × 16 的小番茄图。左边是原始像素位置,右边是按当前偏移量重排后的效果。拖动滑块改变偏移量,或者点击“播放”看它连续变化。
16 × 16 像素重排演示
仅演示位置变化,不处理真实图片原始像素顺序
偏移后的画面
偏移量为 0 时,右图和左图一致;偏移量越大,画面散得越开。真实工具会根据图片的总像素数自动计算偏移量,不需要手动设置。
编码与解码,是同一件事的两个方向
设 curve[i] 表示曲线上第 i 个位置对应的像素下标,N = W × H。编码和解码可以用同一段循环表达,只是搬运方向相反:
offset = round(0.6180339887 × N)
# 混淆
for i in 0 .. N-1:
src = curve[i]
dst = curve[(i + offset) % N]
out[dst] = in[src]
# 解混淆
for i in 0 .. N-1:
src = curve[(i + offset) % N]
dst = curve[i]
out[dst] = in[src]
为什么一定能还原?因为“整体平移 offset 位”是一个置换:每个位置恰好被搬走一次,也恰好被搬来一次,没有两个像素落到同一个位置,也没有位置被漏掉。置换都有逆操作,把 offset 换成 N − offset 再走一遍,或者按上面的解码方向走一遍,就回到了起点。
整个过程不改颜色值,只改位置。所以它是无损的——前提是图片没有被有损压缩、缩放或裁剪。
为什么默认导出 PNG
混淆后的画面是高频噪点,JPG 的有损压缩会在噪点边缘引入细小的颜色偏差。这些偏差在视觉上可能看不出来,但解码时会被当成“原始颜色”搬回去,导致还原结果出现色斑或颗粒。
PNG 使用无损压缩,能原样保存每个像素的 RGBA 值,因此更适合“混淆—还原”这种要求精确往返的场景。代价是文件更大:噪点图很难被压缩,一张手机照片的混淆结果可能会达到原图的数倍。
如果你只是做视觉演示,不需要精确还原,可以在下载后自行转成 JPG;但如果这张图还要被还原,请保留 PNG 原件。
速度、内存与尺寸上限
算法的时间复杂度是 O(N):每个像素只搬运一次。真正占资源的是内存——需要同时保存原图、输出图、曲线顺序和两份像素缓冲区。图片越大,内存压力越明显,手机浏览器尤其容易因为内存不足而中断。
因此工具把单张图片限制在 800 万像素、单边 4000 像素、文件 40MB。超过上限时先等比缩小,再进入处理。这个限制不是算法本身的要求,而是为了让普通设备也能稳定完成。
曲线顺序使用 Uint32Array 存储,比用对象数组节省大量内存;像素搬运直接操作字节缓冲区,避免逐像素创建临时数组。即使这样,超大图仍然可能让页面短暂无响应。
安全边界:可逆不等于保密
这套算法的全部规则都是公开的:曲线怎么走、偏移量怎么算、输出是什么格式。没有密钥,也没有服务端校验。任何人拿到混淆图和本站页面,都可以还原。
所以它适合:挡住聊天窗口里的缩略图、避免预览图被顺手看见、做算法演示、给不需要细节的人看一个占位画面。
它不适合:保护证件、财务信息、商业机密、他人隐私,或任何“被看到就会造成实际损失”的内容。这些场景请使用带密钥的加密工具,并确认接收方的设备和传输渠道安全。