置换本身没有误差,你在还原结果里看到的噪点全部来自图片格式。这一篇先证明算法可逆,再逐层拆开有损压缩到底丢了什么。
要点速记
- 曲线序是双射,位移是轮换,两者都可逆,算法本身逐字节无损。
- 噪点来自 JPEG 的 DCT 变换、量化与色度抽样,不来自置换。
- 置换打乱了空间相关性,压缩误差被摊到全图而非集中在局部。
- 只有全程无损(PNG 进出、不缩放)才能逐字节还原。
置换与双射:先把两个词说清楚
置换就是重排位置:每件东西都有唯一的新座位,既不合并,也不丢弃任何一件。教室换座位是置换,洗牌也是置换。
双射说的是一一对应。用一句话给出数学定义:对集合 S 上的映射 f,如果每个元素 y 都存在唯一的 x 使 f(x) = y,那么 f 就是双射。它意味着既没有两个输入挤到同一个输出,也没有输出被空着。
这个方案里有两层双射。第一层是曲线序 order,把 0 到 N−1 的曲线序号一一对应到 0 到 N−1 的像素下标,每个格子恰好被访问一次;第二层是位移 (i + k) mod N,它是 N 个位置上的一个轮换,同样一一对应。
强调双射是因为它正好是可逆的充要条件:每一步都不丢不重,反过来走就一定回得到起点。名词解释可以查术语表。
证明两步互为逆运算
证明不需要任何图片知识,只要把两段循环的定义写出来再代入。
先写混淆的定义。对每一个曲线序号 i,混淆后的图像 A 满足 A[order[(i + k) mod N]] = in[order[i]],即原图中曲线序号 i 的像素被放到了序号 (i+k) mod N 的位置上。
把上式改写成以落点为主角的形式。令 m = (i + k) mod N,反过来 i 就是 (m − k) mod N,于是对任意曲线序号 m 都有 A[order[m]] = in[order[(m − k) mod N]]。
再写解混淆的定义:解混淆后的图像 B 满足 B[order[i]] = A[order[(i + k) mod N]],它读取的正是 A 在位置 (i+k) mod N 上的像素。
把 m = (i + k) mod N 代入前一段的式子,右边立刻变成 in[order[((i + k) − k) mod N]],也就是 in[order[i]],于是 B[order[i]] = in[order[i]] 对每个 i 都成立。
最后一步:曲线序把 0 到 N−1 全部走了一遍,每个像素下标恰好出现一次,所以对每个曲线序号成立就等价于对每个像素成立,B 与 in 逐像素相同,证明结束。
换个角度更直观:混淆是把一维队列循环右移 k 位,解混淆是循环左移 k 位,右移再左移等于原地不动。位移的循环结构在黄金分割位移里有完整推导:1920×1080 的 N 是 2 073 600,k 是 1 281 555,最大公约数 135,因此置换由 135 个循环组成,每个循环长 15 360。
所以算法本身是严格无损的
推导只用到位置的一一对应,没有用到任何像素值:输入是什么字节,输出就是同一批字节换了位置,一个不多、一个不少。所以不经过有损编码时,混淆再解混淆可以逐字节还原原图,包括 alpha 通道。
另有两个容易忽略的事实。其一,混淆不改变颜色直方图,红色像素的数量前后完全一样,颜色分布这类统计特征仍然暴露原图信息。其二,原有水印、文字、人脸的像素仍然存在于图中,只是位置被移动。
既然算法无损,噪点从哪来?从算法外面来。置换流程与伪代码里那四行赋值没有任何近似成分,问题出在把结果写进文件的那一刻。
损失从哪里来:JPEG 的有损压缩
JPEG 的编码流程粗分为四步:切成 8×8 小块、对每块做 DCT 变换、对系数做量化、熵编码。量化是不可逆的关键一步。
DCT 把小块从像素值转换成一组频率系数:低频描述整体明暗,高频描述细节与边缘。量化是把每个系数除以量化表里对应的步长再取整,步长大的系数常常直接变成 0,这部分信息在解码时永久消失。
量化表按人眼敏感度设计:低频步长小、保留得多,高频步长大、丢得多。被丢掉的正好是文字边缘、毛发、细线这类细节,也正是还原结果里最容易看出问题的地方。
质量参数控制量化表的缩放比例。本站与原工具一致,JPEG 输出的质量参数取 1,这是压缩器允许的最高档;但最高档只是把误差压小,不会归零,JPEG 只要有量化就一定是有损的。
被量化掉的系数在解码阶段无法找回。任何后续处理——包括再走一遍解混淆——都不能恢复这些信息,还原结果只能接近原图,不可能等于原图。
色度抽样为什么造成彩边
JPEG 编码前先把 RGB 拆成亮度 Y 与色度 Cb、Cr。人眼对亮度分辨率敏感、对色度迟钝,于是编码器允许色度降级存储,这就是色度抽样。
常见的 4:2:0 把色度在两个方向上都减半:每 2×2 四个像素共用一组色度值,色度只剩四分之一的分辨率。日常照片里看不出来,但在彩色文字边缘和饱和色细线上非常明显。
原因是色度降采样后又插值放大,颜色会跨越原本的轮廓扩散出去:一列锐利的红色文字,插值后红色渗到相邻像素,还原时就表现为边缘发彩、出现原图没有的颜色,也就是常说的彩边。
置换会把这种误差搬到别处。原图里彩边沿文字轮廓分布,位置集中;解混淆把像素送回原位后,彩边散布全图,不再与任何可见轮廓对应。
| 环节 | PNG | JPEG |
|---|---|---|
| 压缩方式 | 无损,可完全还原 | 有损,DCT 加量化 |
| 色度抽样 | 不涉及 | 常见 4:2:0,色度分辨率减半 |
| 高频细节 | 完整保留 | 被量化抹掉 |
| 还原结果 | 逐字节相同 | 残留噪点与彩边 |
误差为什么会扩散到整张图
因为置换破坏了空间相关性。原图里相邻像素高度相似,压缩误差被局部结构挡住:平滑区域本就平坦,误差看不见;纹理区域误差混在细节里,难以分辨。
混淆之后,画布上相邻的像素在原图里可能相隔几百个像素,彼此毫无关系。JPEG 编码器面对的是一张接近噪声的图,预测与熵编码效率大幅下降,这就是混淆后的 JPEG 通常比原图更大的原因,文件变大不是故障。
解码时每个块里的量化误差已经混进被搬运过的像素。解混淆把它们送回原位,一个 8×8 块的 64 个像素会落到原图里彼此相距很远的位置,原本集中的误差就被均匀撒到全图。
结果就是:错的只是随机位置的单个像素,看起来却像全图都脏了。个别位置还会出现孤立的亮暗点,因为那里原本的邻居帮不上忙。
多次转存的累积效应
每次打开再另存为 JPEG 都会重新走一遍 DCT、量化与色度抽样。误差只会叠加,不会抵消:上一轮丢掉的系数不会因再编码一次而回来,这一轮还会在已经失真的像素上再丢一批。
更麻烦的是链路里常有看不见的重采样:聊天软件的图片压缩、截图工具、社交平台的上传优化,都可能顺手把尺寸改小。缩放要重采样,再插值放大回不到原样,这比 JPEG 量化更难察觉,也更彻底。
一个粗略的观察规律:转存次数越多,色度劣化与边缘模糊越明显;文件体积的变化没有固定方向,但画质只会更差。
如果链路里既缩放又走 JPEG,损伤会被置换进一步摊开:丢失的细节本该集中在被合并的相邻像素上,解混淆后却散落全图,看上去比单纯缩放更糟。
什么时候能做到逐字节还原
条件只有一条:整条链路必须无损,也就是 PNG 进入、PNG 输出、不缩放、不截屏、不经过有损转存,混淆与解混淆之间不落 JPEG。
满足条件时,混淆后的 PNG 里保存的就是原图那批字节,只是位置不同;再解混淆一次,位置换回来,结果与原图逐字节相同。验证方法是做一次无损往返:对同一张 PNG 先混淆得到 A,再对 A 解混淆得到 B,把 B 与原图逐像素比对。
| 链路 | 还原结果 | 原因 |
|---|---|---|
| PNG 进,PNG 出,不缩放 | 逐字节完全相同 | 全程无损 |
| PNG 进,JPEG 出 | 残留噪点与彩边 | 量化与色度抽样 |
| 中间被任何工具缩放过 | 细节永久丢失 | 重采样不可逆 |
| 多次 JPEG 转存 | 劣化逐次累积 | 每轮都重新量化 |
还有一点要提醒:同样的像素用 PNG 保存后体积可能比原图更大,因为空间相关性被破坏后无损压缩的压缩率下降,这属于正常现象,与画质无关。取舍细节可以看输出格式与画质。
实践建议与局限
如果目的是精确还原,全程用 PNG,不要对混淆后的文件做二次处理。已经拿到 JPEG 的图片,不要在还原前先转成 PNG,多一次编码只会多一次损失,直接解混淆更好。
如果只是让内容不适合被直接预览,接受 JPEG 的损失是合理的,前提是别指望拿回完全一致的像素。要把噪点压到最低就减少链路环节:不缩放、不截图、不二次发送。
解混淆不是修复工具。对已经被 JPEG 严重损伤的图片,解混淆无法去噪,也无法补回高频细节;去噪与锐化属于另一类图像处理,与置换没有关系。
算法层面的局限也要说清楚。这套置换没有密钥,规则公开且可逆,任何人都能解混淆:它提高的是自动化直接复用的成本,不能阻止人工还原,也不能当加密用。混淆也不删除信息,水印、文字、人脸的像素都还在图中,只是换了位置,真正的脱敏必须先永久遮挡或裁剪,边界见社交分享中的图片隐私处理。想对比两种格式的差异,可以直接去图片混淆 / 解混淆工作台。
本文更新于 2026-10-14。文中算法说明基于本站实际运行的实现整理,行为可通过 _build/verify-core.mjs 复核。
内容为本站原创,转载或引用请注明来源「小番茄混淆解图」并保留原文链接。