在一些社群看到的“乱码”图是啥,以及针对的一点改进

2026-04-11 14:581阅读0评论SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐
问题描述:

有时候在一些聊天分享的社区/群聊里面会看到类似的图像

image1020×618 107 KB

这种看上去纯乱码的图片是什么呢?

这其实是一种利用了 Gilbert 曲线(广义希尔伯特曲线) 的图像混淆(至于为啥要混淆那心照不宣)
e974199a120267e22f6f88a1a4dd2ef9602×624 41.7 KB

但是这样混淆掉的图像也有很明显的问题:别人压根不知道解开后里面会是啥内容,万一开到一些不好的把人吓出心理阴影咋办

基于此,我又在这个基础上加了一层小预览机制

  • 先把原图按 tile 切块
  • 按固定步长抽样一些 tile
  • 把这些 tile 重新拼到左上角
  • 形成一个低分辨率预览
  • 剩下区域再做 Gilbert 路径混淆

这样最后出来的图就很有意思:

  • 左上角还能大致看出原图内容
  • 主体区域已经被混淆

就像这样:

unmuddled1024×1024 377 KB
muddled1026×1026 532 KB

也可以调整的更抽象一些(细节更低、尺寸更小),这里为了方便看出来我调的比较清晰,实际上可用很抽象的:
image1491×1380 485 KB

Python的实现逻辑:

#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ image_muddle_gilbert.py CLI 示例: # 编码:保留左上角预览,非预览区使用 Gilbert 曲线路径做循环位移混肴 python image_muddle_gilbert.py encode --in in.jpg --out muddled.png \ --tile 32 32 --stride 4 4 --key 123456 --embed-meta # 解码(若 PNG 已嵌入元数据) python image_muddle_gilbert.py decode --in muddled.png --out restored.png # 或解码时显式提供 JSON 元数据 python image_muddle_gilbert.py decode --in muddled.png --out restored.png --meta meta.json """ from __future__ import annotations import argparse import json import math import random from dataclasses import dataclass, asdict from typing import List, Tuple, Dict, Any import numpy as np from PIL import Image, PngImagePlugin def _sign(x: int) -> int: return (x > 0) - (x < 0) def _generate2d(x: int, y: int, ax: int, ay: int, bx: int, by: int, coords: List[Tuple[int, int]]) -> None: w = abs(ax + ay) h = abs(bx + by) dax, day = _sign(ax), _sign(ay) # 主方向的单位步长 dbx, dby = _sign(bx), _sign(by) # 正交方向的单位步长 if h == 1: # 一行填充 for _ in range(w): coords.append((x, y)) x += dax y += day return if w == 1: # 一列填充 for _ in range(h): coords.append((x, y)) x += dbx y += dby return ax2, ay2 = ax // 2, ay // 2 bx2, by2 = bx // 2, by // 2 w2 = abs(ax2 + ay2) h2 = abs(bx2 + by2) if 2 * w > 3 * h: if (w2 % 2) and (w > 2): # 偏好偶数步长 ax2 += dax ay2 += day # “长”情形:分两段 _generate2d(x, y, ax2, ay2, bx, by, coords) _generate2d(x + ax2, y + ay2, ax - ax2, ay - ay2, bx, by, coords) else: if (h2 % 2) and (h > 2): # 偏好偶数步长 bx2 += dbx by2 += dby # 标准情形:上一步 + 长水平 + 下一步 _generate2d(x, y, bx2, by2, ax2, ay2, coords) _generate2d(x + bx2, y + by2, ax, ay, bx - bx2, by - by2, coords) _generate2d(x + (ax - dax) + (bx2 - dbx), y + (ay - day) + (by2 - dby), -bx2, -by2, -(ax - ax2), -(ay - ay2), coords) def gilbert2d(width: int, height: int) -> List[Tuple[int, int]]: """返回按广义 Hilbert 曲线遍历 width×height 矩形的像素坐标序列 (x,y)。""" coordinates: List[Tuple[int, int]] = [] if width >= height: _generate2d(0, 0, width, 0, 0, height, coordinates) else: _generate2d(0, 0, 0, height, width, 0, coordinates) return coordinates @dataclass class MuddleMetaV2: version: int # 2 algo: str # 'gilbert_shift' orig_size: Tuple[int, int] # (H, W) padded_size: Tuple[int, int] # (Hp, Wp) mode: str # 原图模式(如 'RGB') tile_size: Tuple[int, int] # (tile_h, tile_w) grid_size: Tuple[int, int] # (R, C) stride: Tuple[int, int] # (stride_y, stride_x) preview_grid_size: Tuple[int, int] # (Pr, Pc) pad_mode: str # 预览抽样结果(原 tile 坐标) preview_tiles: List[Tuple[int, int]] # Gilbert 路径的“循环位移”参数;这里只记录 offset 与 M,便于分析/互操作 perm: Dict[str, int] # {'offset': O, 'M': M} notes: str # 说明 def _to_array(img: Image.Image) -> np.ndarray: """PIL Image -> (H, W, 4) RGBA uint8""" if img.mode != "RGBA": img = img.convert("RGBA") return np.array(img, dtype=np.uint8) def _pad_to_multiple(arr: np.ndarray, th: int, tw: int, pad_mode: str) -> Tuple[int, int, np.ndarray]: """把图像 pad 成 tile 的整数倍尺寸。""" H, W, C = arr.shape ph = (th - (H % th)) % th pw = (tw - (W % tw)) % tw if ph == 0 and pw == 0: return H, W, arr if pad_mode == "edge": arr2 = np.pad(arr, ((0, ph), (0, pw), (0, 0)), mode="edge") elif pad_mode == "constant": arr2 = np.pad(arr, ((0, ph), (0, pw), (0, 0)), mode="constant", constant_values=0) else: raise ValueError(f"未知 pad_mode: {pad_mode}") return H + ph, W + pw, arr2 def _copy_tile(src: np.ndarray, dst: np.ndarray, src_tile: Tuple[int, int], dst_tile: Tuple[int, int], tile_size: Tuple[int, int]) -> None: """以 tile 为单位拷贝(按像素)""" th, tw = tile_size sr, sc = src_tile dr, dc = dst_tile sy0, sy1 = sr * th, (sr + 1) * th sx0, sx1 = sc * tw, (sc + 1) * tw dy0, dy1 = dr * th, (dr + 1) * th dx0, dx1 = dc * tw, (dc + 1) * tw dst[dy0:dy1, dx0:dx1, :] = src[sy0:sy1, sx0:sx1, :] def _deriv_offset(M: int, key: int) -> int: """ 从 key 推导循环位移 offset。 参考小番茄实现:offset ≈ round(phi * (W*H)),phi = (sqrt(5)-1)/2。 这里在此基础上加上 key 的扰动,避免 0 与可预测性。 """ if M <= 1: return 0 phi = (math.sqrt(5.0) - 1.0) / 2.0 base = int(round(phi * M)) # 线性同余扰动,让不同 key 有不同 offset;确保落在 [1, M-1] rnd = (1103515245 * (key & 0xFFFFFFFF) + 12345) & 0x7FFFFFFF offset = (base + rnd) % M if offset == 0: offset = 1 return offset class ImageMuddlerGilbert: def __init__( self, tile_h: int = 32, tile_w: int = 32, stride_y: int = 4, stride_x: int = 4, key: int = 123456, pad_mode: str = "edge", ): assert tile_h > 0 and tile_w > 0 assert stride_y > 0 and stride_x > 0 self.tile_h = tile_h self.tile_w = tile_w self.stride_y = stride_y self.stride_x = stride_x self.key = key self.pad_mode = pad_mode def encode(self, img: Image.Image) -> Tuple[Image.Image, MuddleMetaV2]: mode = img.mode arr = _to_array(img) H, W, _ = arr.shape Hp, Wp, arr_padded = _pad_to_multiple(arr, self.tile_h, self.tile_w, self.pad_mode) R = Hp // self.tile_h Cc = Wp // self.tile_w # 预览 tile:按 stride 抽样 preview_tiles = [(r, c) for r in range(0, R, self.stride_y) for c in range(0, Cc, self.stride_x)] Pr = len(range(0, R, self.stride_y)) Pc = len(range(0, Cc, self.stride_x)) out = np.zeros_like(arr_padded) # 1) 拷贝预览:将 preview_tiles(原图位置)复制到输出左上角的 Pr×Pc 个 tile for idx, (tr, tc) in enumerate(preview_tiles): pr = idx // Pc pc = idx % Pc _copy_tile(arr_padded, out, src_tile=(tr, tc), dst_tile=(pr, pc), tile_size=(self.tile_h, self.tile_w)) # 2) 生成两套 Gilbert 序列 is_preview_tile = np.zeros((R, Cc), dtype=bool) for tr, tc in preview_tiles: is_preview_tile[tr, tc] = True preH = Pr * self.tile_h preW = Pc * self.tile_w # 全图的 Gilbert 曲线(x,y) path_xy = gilbert2d(Wp, Hp) # (x, y) def in_preview_tile_xy(x: int, y: int) -> bool: tr = y // self.tile_h tc = x // self.tile_w return is_preview_tile[tr, tc] # source:排除“原始预览 tile” source_lin = [y * Wp + x for (x, y) in path_xy if not in_preview_tile_xy(x, y)] # dest :排除“左上角预览矩形” dest_lin = [y * Wp + x for (x, y) in path_xy if not (y < preH and x < preW)] if len(source_lin) != len(dest_lin): raise RuntimeError(f"像素数量不一致:source={len(source_lin)} vs dest={len(dest_lin)}") M = len(source_lin) if M > 0: offset = _deriv_offset(M, self.key) source_idx = np.asarray(source_lin, dtype=np.int64) dest_idx = np.asarray(dest_lin, dtype=np.int64) dest_rot = np.roll(dest_idx, -offset) # 对应 j = (i+offset) % M flat_in = arr_padded.reshape(-1, arr_padded.shape[2]) flat_out = out.reshape(-1, out.shape[2]) flat_out[dest_rot] = flat_in[source_idx] else: offset = 0 meta = MuddleMetaV2( version=2, algo="gilbert_shift", orig_size=(H, W), padded_size=(Hp, Wp), mode=mode, tile_size=(self.tile_h, self.tile_w), grid_size=(R, Cc), stride=(self.stride_y, self.stride_x), preview_grid_size=(Pr, Pc), pad_mode=self.pad_mode, preview_tiles=preview_tiles, perm={"offset": int(offset), "M": int(M)}, notes="source: 全图 Gilbert 排除原始预览 tile;dest: 全图 Gilbert 排除预览矩形;对齐顺序后做循环位移。" ) out_img = Image.fromarray(out, mode="RGBA").convert(mode) return out_img, meta def decode(self, muddled_img: Image.Image, meta: MuddleMetaV2) -> Image.Image: if meta.version != 2 or meta.algo != "gilbert_shift": raise ValueError("该元数据不是 gilbert_shift v2 格式,无法解码。") mode = meta.mode arr_in = _to_array(muddled_img) Hp, Wp = meta.padded_size if (arr_in.shape[0], arr_in.shape[1]) != (Hp, Wp): raise ValueError(f"输入混肴图尺寸 {arr_in.shape[:2]} 与元数据不符 {meta.padded_size}") out = np.zeros_like(arr_in) R, Cc = meta.grid_size tile_h, tile_w = meta.tile_size Pr, Pc = meta.preview_grid_size # 1) 还原预览:将左上角 Pr×Pc 个 tile 拷回原始预览 tile 的位置 preview_tiles = meta.preview_tiles for idx, (tr, tc) in enumerate(preview_tiles): pr = idx // Pc pc = idx % Pc _copy_tile(arr_in, out, src_tile=(pr, pc), dst_tile=(tr, tc), tile_size=(tile_h, tile_w)) # 2) 生成两套 Gilbert 序列(与 encode 完全一致) is_preview_tile = np.zeros((R, Cc), dtype=bool) for tr, tc in preview_tiles: is_preview_tile[tr, tc] = True preH = Pr * tile_h preW = Pc * tile_w path_xy = gilbert2d(Wp, Hp) def in_preview_tile_xy(x: int, y: int) -> bool: tr = y // tile_h tc = x // tile_w return is_preview_tile[tr, tc] source_lin = [y * Wp + x for (x, y) in path_xy if not in_preview_tile_xy(x, y)] dest_lin = [y * Wp + x for (x, y) in path_xy if not (y < preH and x < preW)] M = len(source_lin) if M != meta.perm["M"] or M != len(dest_lin): raise ValueError("元数据 M 与当前尺寸/参数不一致,无法解码。") offset = int(meta.perm["offset"]) if M > 0 else 0 if M > 0: source_idx = np.asarray(source_lin, dtype=np.int64) dest_idx = np.asarray(dest_lin, dtype=np.int64) dest_rot = np.roll(dest_idx, -offset) # encode 写入位置 flat_in = arr_in.reshape(-1, arr_in.shape[2]) flat_out = out.reshape(-1, out.shape[2]) # 解码:把 in[dest_rot] 放回 out[source_idx] flat_out[source_idx] = flat_in[dest_rot] # 裁剪回原尺寸 H, W = meta.orig_size out = out[:H, :W, :] return Image.fromarray(out, mode="RGBA").convert(mode) PNG_TEXT_KEY = "muddle_meta" def save_with_meta(img: Image.Image, out_path: str, meta: MuddleMetaV2, embed: bool) -> None: if embed: if not out_path.lower().endswith(".png"): raise ValueError("只有 PNG 支持嵌入元数据,请将输出文件扩展名设为 .png 或关闭 --embed-meta") pnginfo = PngImagePlugin.PngInfo() pnginfo.add_text(PNG_TEXT_KEY, json.dumps(asdict(meta), ensure_ascii=False)) img.save(out_path, pnginfo=pnginfo) else: img.save(out_path) def try_load_meta_from_png(png_path: str) -> Dict[str, Any] | None: im = Image.open(png_path) if hasattr(im, "text") and PNG_TEXT_KEY in im.text: raw = im.text[PNG_TEXT_KEY] return json.loads(raw) return None def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( description="图像混肴(可逆):左上角预览 + Gilbert 曲线路径循环位移;支持 encode/decode", formatter_class=argparse.ArgumentDefaultsHelpFormatter ) subp = p.add_subparsers(dest="cmd", required=True) pe = subp.add_parser("encode", help="混肴图像") pe.add_argument("--in", dest="inp", required=True, help="输入图像路径") pe.add_argument("--out", dest="out", required=True, help="输出混肴图路径(PNG/JPG 等)") pe.add_argument("--meta", dest="meta", default=None, help="元数据 JSON 输出路径(若 --embed-meta 则可不写)") pe.add_argument("--embed-meta", action="store_true", help="把元数据嵌入 PNG(tEXt)") pe.add_argument("--tile", nargs=2, type=int, default=[32, 32], metavar=("H", "W"), help="tile 像素尺寸") pe.add_argument("--stride", nargs=2, type=int, default=[4, 4], metavar=("Y", "X"), help="预览抽样步长(按 tile)") pe.add_argument("--key", type=int, default=123456, help="位移密钥(影响 offset )") pe.add_argument("--pad-mode", choices=["edge", "constant"], default="edge", help="边界填充方式") pd = subp.add_parser("decode", help="还原混肴图像") pd.add_argument("--in", dest="inp", required=True, help="输入混肴图(PNG/JPG 等)") pd.add_argument("--out", dest="out", required=True, help="输出还原图路径") pd.add_argument("--meta", dest="meta", default=None, help="元数据 JSON 路径(若 PNG 内已嵌入则可省略)") p.add_argument("--selftest", action="store_true", help="运行一个自检:随机图像 encode->decode 完整性检查") return p def main(): parser = build_parser() args = parser.parse_args() if args.selftest: # 自检:随机图像往返测试 H, W = 64, 96 rnd = np.random.default_rng(42) arr = (rnd.integers(0, 256, size=(H, W, 3), dtype=np.uint8)) img = Image.fromarray(arr, mode="RGB") muddler = ImageMuddlerGilbert( tile_h=16, tile_w=16, stride_y=4, stride_x=4, key=2024, pad_mode="edge" ) enc_img, meta = muddler.encode(img) dec_img = muddler.decode(enc_img, meta) ok = np.array_equal(np.array(dec_img), np.array(img)) print("SELFTEST:", "PASS" if ok else "FAIL") return if args.cmd == "encode": img = Image.open(args.inp) tile_h, tile_w = args.tile stride_y, stride_x = args.stride muddler = ImageMuddlerGilbert( tile_h=tile_h, tile_w=tile_w, stride_y=stride_y, stride_x=stride_x, key=args.key, pad_mode=args.pad_mode, ) out_img, meta = muddler.encode(img) # 保存图像 save_with_meta(out_img, args.out, meta, embed=args.embed_meta) # 保存/输出元数据 if args.meta: with open(args.meta, "w", encoding="utf-8") as f: json.dump(asdict(meta), f, ensure_ascii=False, indent=2) elif not args.embed_meta: print("⚠ 未保存元数据:建议使用 --meta 输出 JSON 或使用 --embed-meta 嵌入 PNG") else: print("✅ 元数据已嵌入 PNG tEXt 块") print("完成:", args.out) elif args.cmd == "decode": # 先尝试从 PNG 里读元数据 meta_dict = None if args.meta is None and args.inp.lower().endswith(".png"): meta_dict = try_load_meta_from_png(args.inp) if meta_dict is not None: print("🔎 已从 PNG 读取嵌入的元数据") if meta_dict is None: if args.meta is None: raise SystemExit("需要元数据:请提供 --meta JSON,或输入 PNG 内需已嵌入元数据") with open(args.meta, "r", encoding="utf-8") as f: meta_dict = json.load(f) if meta_dict.get("version") != 2 or meta_dict.get("algo") != "gilbert_shift": raise SystemExit("该元数据不是 gilbert_shift v2 格式,无法解码。") meta = MuddleMetaV2(**meta_dict) img = Image.open(args.inp) muddler = ImageMuddlerGilbert( tile_h=meta.tile_size[0], tile_w=meta.tile_size[1], stride_y=meta.stride[0], stride_x=meta.stride[1], key=0, # decode 不依赖 key pad_mode=meta.pad_mode, ) out_img = muddler.decode(img, meta) out_img.save(args.out) print("完成:", args.out) if __name__ == "__main__": main()

新人小小水个帖子~

f4f4454a36e8c6d6ed9426f1dec7b7f91440×1062 196 KB

网友解答:
--【壹】--:

有时候在一些聊天分享的社区/群聊里面会看到类似的图像

image1020×618 107 KB

这种看上去纯乱码的图片是什么呢?

这其实是一种利用了 Gilbert 曲线(广义希尔伯特曲线) 的图像混淆(至于为啥要混淆那心照不宣)
e974199a120267e22f6f88a1a4dd2ef9602×624 41.7 KB

但是这样混淆掉的图像也有很明显的问题:别人压根不知道解开后里面会是啥内容,万一开到一些不好的把人吓出心理阴影咋办

基于此,我又在这个基础上加了一层小预览机制

  • 先把原图按 tile 切块
  • 按固定步长抽样一些 tile
  • 把这些 tile 重新拼到左上角
  • 形成一个低分辨率预览
  • 剩下区域再做 Gilbert 路径混淆

这样最后出来的图就很有意思:

  • 左上角还能大致看出原图内容
  • 主体区域已经被混淆

就像这样:

unmuddled1024×1024 377 KB
muddled1026×1026 532 KB

也可以调整的更抽象一些(细节更低、尺寸更小),这里为了方便看出来我调的比较清晰,实际上可用很抽象的:
image1491×1380 485 KB

Python的实现逻辑:

#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ image_muddle_gilbert.py CLI 示例: # 编码:保留左上角预览,非预览区使用 Gilbert 曲线路径做循环位移混肴 python image_muddle_gilbert.py encode --in in.jpg --out muddled.png \ --tile 32 32 --stride 4 4 --key 123456 --embed-meta # 解码(若 PNG 已嵌入元数据) python image_muddle_gilbert.py decode --in muddled.png --out restored.png # 或解码时显式提供 JSON 元数据 python image_muddle_gilbert.py decode --in muddled.png --out restored.png --meta meta.json """ from __future__ import annotations import argparse import json import math import random from dataclasses import dataclass, asdict from typing import List, Tuple, Dict, Any import numpy as np from PIL import Image, PngImagePlugin def _sign(x: int) -> int: return (x > 0) - (x < 0) def _generate2d(x: int, y: int, ax: int, ay: int, bx: int, by: int, coords: List[Tuple[int, int]]) -> None: w = abs(ax + ay) h = abs(bx + by) dax, day = _sign(ax), _sign(ay) # 主方向的单位步长 dbx, dby = _sign(bx), _sign(by) # 正交方向的单位步长 if h == 1: # 一行填充 for _ in range(w): coords.append((x, y)) x += dax y += day return if w == 1: # 一列填充 for _ in range(h): coords.append((x, y)) x += dbx y += dby return ax2, ay2 = ax // 2, ay // 2 bx2, by2 = bx // 2, by // 2 w2 = abs(ax2 + ay2) h2 = abs(bx2 + by2) if 2 * w > 3 * h: if (w2 % 2) and (w > 2): # 偏好偶数步长 ax2 += dax ay2 += day # “长”情形:分两段 _generate2d(x, y, ax2, ay2, bx, by, coords) _generate2d(x + ax2, y + ay2, ax - ax2, ay - ay2, bx, by, coords) else: if (h2 % 2) and (h > 2): # 偏好偶数步长 bx2 += dbx by2 += dby # 标准情形:上一步 + 长水平 + 下一步 _generate2d(x, y, bx2, by2, ax2, ay2, coords) _generate2d(x + bx2, y + by2, ax, ay, bx - bx2, by - by2, coords) _generate2d(x + (ax - dax) + (bx2 - dbx), y + (ay - day) + (by2 - dby), -bx2, -by2, -(ax - ax2), -(ay - ay2), coords) def gilbert2d(width: int, height: int) -> List[Tuple[int, int]]: """返回按广义 Hilbert 曲线遍历 width×height 矩形的像素坐标序列 (x,y)。""" coordinates: List[Tuple[int, int]] = [] if width >= height: _generate2d(0, 0, width, 0, 0, height, coordinates) else: _generate2d(0, 0, 0, height, width, 0, coordinates) return coordinates @dataclass class MuddleMetaV2: version: int # 2 algo: str # 'gilbert_shift' orig_size: Tuple[int, int] # (H, W) padded_size: Tuple[int, int] # (Hp, Wp) mode: str # 原图模式(如 'RGB') tile_size: Tuple[int, int] # (tile_h, tile_w) grid_size: Tuple[int, int] # (R, C) stride: Tuple[int, int] # (stride_y, stride_x) preview_grid_size: Tuple[int, int] # (Pr, Pc) pad_mode: str # 预览抽样结果(原 tile 坐标) preview_tiles: List[Tuple[int, int]] # Gilbert 路径的“循环位移”参数;这里只记录 offset 与 M,便于分析/互操作 perm: Dict[str, int] # {'offset': O, 'M': M} notes: str # 说明 def _to_array(img: Image.Image) -> np.ndarray: """PIL Image -> (H, W, 4) RGBA uint8""" if img.mode != "RGBA": img = img.convert("RGBA") return np.array(img, dtype=np.uint8) def _pad_to_multiple(arr: np.ndarray, th: int, tw: int, pad_mode: str) -> Tuple[int, int, np.ndarray]: """把图像 pad 成 tile 的整数倍尺寸。""" H, W, C = arr.shape ph = (th - (H % th)) % th pw = (tw - (W % tw)) % tw if ph == 0 and pw == 0: return H, W, arr if pad_mode == "edge": arr2 = np.pad(arr, ((0, ph), (0, pw), (0, 0)), mode="edge") elif pad_mode == "constant": arr2 = np.pad(arr, ((0, ph), (0, pw), (0, 0)), mode="constant", constant_values=0) else: raise ValueError(f"未知 pad_mode: {pad_mode}") return H + ph, W + pw, arr2 def _copy_tile(src: np.ndarray, dst: np.ndarray, src_tile: Tuple[int, int], dst_tile: Tuple[int, int], tile_size: Tuple[int, int]) -> None: """以 tile 为单位拷贝(按像素)""" th, tw = tile_size sr, sc = src_tile dr, dc = dst_tile sy0, sy1 = sr * th, (sr + 1) * th sx0, sx1 = sc * tw, (sc + 1) * tw dy0, dy1 = dr * th, (dr + 1) * th dx0, dx1 = dc * tw, (dc + 1) * tw dst[dy0:dy1, dx0:dx1, :] = src[sy0:sy1, sx0:sx1, :] def _deriv_offset(M: int, key: int) -> int: """ 从 key 推导循环位移 offset。 参考小番茄实现:offset ≈ round(phi * (W*H)),phi = (sqrt(5)-1)/2。 这里在此基础上加上 key 的扰动,避免 0 与可预测性。 """ if M <= 1: return 0 phi = (math.sqrt(5.0) - 1.0) / 2.0 base = int(round(phi * M)) # 线性同余扰动,让不同 key 有不同 offset;确保落在 [1, M-1] rnd = (1103515245 * (key & 0xFFFFFFFF) + 12345) & 0x7FFFFFFF offset = (base + rnd) % M if offset == 0: offset = 1 return offset class ImageMuddlerGilbert: def __init__( self, tile_h: int = 32, tile_w: int = 32, stride_y: int = 4, stride_x: int = 4, key: int = 123456, pad_mode: str = "edge", ): assert tile_h > 0 and tile_w > 0 assert stride_y > 0 and stride_x > 0 self.tile_h = tile_h self.tile_w = tile_w self.stride_y = stride_y self.stride_x = stride_x self.key = key self.pad_mode = pad_mode def encode(self, img: Image.Image) -> Tuple[Image.Image, MuddleMetaV2]: mode = img.mode arr = _to_array(img) H, W, _ = arr.shape Hp, Wp, arr_padded = _pad_to_multiple(arr, self.tile_h, self.tile_w, self.pad_mode) R = Hp // self.tile_h Cc = Wp // self.tile_w # 预览 tile:按 stride 抽样 preview_tiles = [(r, c) for r in range(0, R, self.stride_y) for c in range(0, Cc, self.stride_x)] Pr = len(range(0, R, self.stride_y)) Pc = len(range(0, Cc, self.stride_x)) out = np.zeros_like(arr_padded) # 1) 拷贝预览:将 preview_tiles(原图位置)复制到输出左上角的 Pr×Pc 个 tile for idx, (tr, tc) in enumerate(preview_tiles): pr = idx // Pc pc = idx % Pc _copy_tile(arr_padded, out, src_tile=(tr, tc), dst_tile=(pr, pc), tile_size=(self.tile_h, self.tile_w)) # 2) 生成两套 Gilbert 序列 is_preview_tile = np.zeros((R, Cc), dtype=bool) for tr, tc in preview_tiles: is_preview_tile[tr, tc] = True preH = Pr * self.tile_h preW = Pc * self.tile_w # 全图的 Gilbert 曲线(x,y) path_xy = gilbert2d(Wp, Hp) # (x, y) def in_preview_tile_xy(x: int, y: int) -> bool: tr = y // self.tile_h tc = x // self.tile_w return is_preview_tile[tr, tc] # source:排除“原始预览 tile” source_lin = [y * Wp + x for (x, y) in path_xy if not in_preview_tile_xy(x, y)] # dest :排除“左上角预览矩形” dest_lin = [y * Wp + x for (x, y) in path_xy if not (y < preH and x < preW)] if len(source_lin) != len(dest_lin): raise RuntimeError(f"像素数量不一致:source={len(source_lin)} vs dest={len(dest_lin)}") M = len(source_lin) if M > 0: offset = _deriv_offset(M, self.key) source_idx = np.asarray(source_lin, dtype=np.int64) dest_idx = np.asarray(dest_lin, dtype=np.int64) dest_rot = np.roll(dest_idx, -offset) # 对应 j = (i+offset) % M flat_in = arr_padded.reshape(-1, arr_padded.shape[2]) flat_out = out.reshape(-1, out.shape[2]) flat_out[dest_rot] = flat_in[source_idx] else: offset = 0 meta = MuddleMetaV2( version=2, algo="gilbert_shift", orig_size=(H, W), padded_size=(Hp, Wp), mode=mode, tile_size=(self.tile_h, self.tile_w), grid_size=(R, Cc), stride=(self.stride_y, self.stride_x), preview_grid_size=(Pr, Pc), pad_mode=self.pad_mode, preview_tiles=preview_tiles, perm={"offset": int(offset), "M": int(M)}, notes="source: 全图 Gilbert 排除原始预览 tile;dest: 全图 Gilbert 排除预览矩形;对齐顺序后做循环位移。" ) out_img = Image.fromarray(out, mode="RGBA").convert(mode) return out_img, meta def decode(self, muddled_img: Image.Image, meta: MuddleMetaV2) -> Image.Image: if meta.version != 2 or meta.algo != "gilbert_shift": raise ValueError("该元数据不是 gilbert_shift v2 格式,无法解码。") mode = meta.mode arr_in = _to_array(muddled_img) Hp, Wp = meta.padded_size if (arr_in.shape[0], arr_in.shape[1]) != (Hp, Wp): raise ValueError(f"输入混肴图尺寸 {arr_in.shape[:2]} 与元数据不符 {meta.padded_size}") out = np.zeros_like(arr_in) R, Cc = meta.grid_size tile_h, tile_w = meta.tile_size Pr, Pc = meta.preview_grid_size # 1) 还原预览:将左上角 Pr×Pc 个 tile 拷回原始预览 tile 的位置 preview_tiles = meta.preview_tiles for idx, (tr, tc) in enumerate(preview_tiles): pr = idx // Pc pc = idx % Pc _copy_tile(arr_in, out, src_tile=(pr, pc), dst_tile=(tr, tc), tile_size=(tile_h, tile_w)) # 2) 生成两套 Gilbert 序列(与 encode 完全一致) is_preview_tile = np.zeros((R, Cc), dtype=bool) for tr, tc in preview_tiles: is_preview_tile[tr, tc] = True preH = Pr * tile_h preW = Pc * tile_w path_xy = gilbert2d(Wp, Hp) def in_preview_tile_xy(x: int, y: int) -> bool: tr = y // tile_h tc = x // tile_w return is_preview_tile[tr, tc] source_lin = [y * Wp + x for (x, y) in path_xy if not in_preview_tile_xy(x, y)] dest_lin = [y * Wp + x for (x, y) in path_xy if not (y < preH and x < preW)] M = len(source_lin) if M != meta.perm["M"] or M != len(dest_lin): raise ValueError("元数据 M 与当前尺寸/参数不一致,无法解码。") offset = int(meta.perm["offset"]) if M > 0 else 0 if M > 0: source_idx = np.asarray(source_lin, dtype=np.int64) dest_idx = np.asarray(dest_lin, dtype=np.int64) dest_rot = np.roll(dest_idx, -offset) # encode 写入位置 flat_in = arr_in.reshape(-1, arr_in.shape[2]) flat_out = out.reshape(-1, out.shape[2]) # 解码:把 in[dest_rot] 放回 out[source_idx] flat_out[source_idx] = flat_in[dest_rot] # 裁剪回原尺寸 H, W = meta.orig_size out = out[:H, :W, :] return Image.fromarray(out, mode="RGBA").convert(mode) PNG_TEXT_KEY = "muddle_meta" def save_with_meta(img: Image.Image, out_path: str, meta: MuddleMetaV2, embed: bool) -> None: if embed: if not out_path.lower().endswith(".png"): raise ValueError("只有 PNG 支持嵌入元数据,请将输出文件扩展名设为 .png 或关闭 --embed-meta") pnginfo = PngImagePlugin.PngInfo() pnginfo.add_text(PNG_TEXT_KEY, json.dumps(asdict(meta), ensure_ascii=False)) img.save(out_path, pnginfo=pnginfo) else: img.save(out_path) def try_load_meta_from_png(png_path: str) -> Dict[str, Any] | None: im = Image.open(png_path) if hasattr(im, "text") and PNG_TEXT_KEY in im.text: raw = im.text[PNG_TEXT_KEY] return json.loads(raw) return None def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( description="图像混肴(可逆):左上角预览 + Gilbert 曲线路径循环位移;支持 encode/decode", formatter_class=argparse.ArgumentDefaultsHelpFormatter ) subp = p.add_subparsers(dest="cmd", required=True) pe = subp.add_parser("encode", help="混肴图像") pe.add_argument("--in", dest="inp", required=True, help="输入图像路径") pe.add_argument("--out", dest="out", required=True, help="输出混肴图路径(PNG/JPG 等)") pe.add_argument("--meta", dest="meta", default=None, help="元数据 JSON 输出路径(若 --embed-meta 则可不写)") pe.add_argument("--embed-meta", action="store_true", help="把元数据嵌入 PNG(tEXt)") pe.add_argument("--tile", nargs=2, type=int, default=[32, 32], metavar=("H", "W"), help="tile 像素尺寸") pe.add_argument("--stride", nargs=2, type=int, default=[4, 4], metavar=("Y", "X"), help="预览抽样步长(按 tile)") pe.add_argument("--key", type=int, default=123456, help="位移密钥(影响 offset )") pe.add_argument("--pad-mode", choices=["edge", "constant"], default="edge", help="边界填充方式") pd = subp.add_parser("decode", help="还原混肴图像") pd.add_argument("--in", dest="inp", required=True, help="输入混肴图(PNG/JPG 等)") pd.add_argument("--out", dest="out", required=True, help="输出还原图路径") pd.add_argument("--meta", dest="meta", default=None, help="元数据 JSON 路径(若 PNG 内已嵌入则可省略)") p.add_argument("--selftest", action="store_true", help="运行一个自检:随机图像 encode->decode 完整性检查") return p def main(): parser = build_parser() args = parser.parse_args() if args.selftest: # 自检:随机图像往返测试 H, W = 64, 96 rnd = np.random.default_rng(42) arr = (rnd.integers(0, 256, size=(H, W, 3), dtype=np.uint8)) img = Image.fromarray(arr, mode="RGB") muddler = ImageMuddlerGilbert( tile_h=16, tile_w=16, stride_y=4, stride_x=4, key=2024, pad_mode="edge" ) enc_img, meta = muddler.encode(img) dec_img = muddler.decode(enc_img, meta) ok = np.array_equal(np.array(dec_img), np.array(img)) print("SELFTEST:", "PASS" if ok else "FAIL") return if args.cmd == "encode": img = Image.open(args.inp) tile_h, tile_w = args.tile stride_y, stride_x = args.stride muddler = ImageMuddlerGilbert( tile_h=tile_h, tile_w=tile_w, stride_y=stride_y, stride_x=stride_x, key=args.key, pad_mode=args.pad_mode, ) out_img, meta = muddler.encode(img) # 保存图像 save_with_meta(out_img, args.out, meta, embed=args.embed_meta) # 保存/输出元数据 if args.meta: with open(args.meta, "w", encoding="utf-8") as f: json.dump(asdict(meta), f, ensure_ascii=False, indent=2) elif not args.embed_meta: print("⚠ 未保存元数据:建议使用 --meta 输出 JSON 或使用 --embed-meta 嵌入 PNG") else: print("✅ 元数据已嵌入 PNG tEXt 块") print("完成:", args.out) elif args.cmd == "decode": # 先尝试从 PNG 里读元数据 meta_dict = None if args.meta is None and args.inp.lower().endswith(".png"): meta_dict = try_load_meta_from_png(args.inp) if meta_dict is not None: print("🔎 已从 PNG 读取嵌入的元数据") if meta_dict is None: if args.meta is None: raise SystemExit("需要元数据:请提供 --meta JSON,或输入 PNG 内需已嵌入元数据") with open(args.meta, "r", encoding="utf-8") as f: meta_dict = json.load(f) if meta_dict.get("version") != 2 or meta_dict.get("algo") != "gilbert_shift": raise SystemExit("该元数据不是 gilbert_shift v2 格式,无法解码。") meta = MuddleMetaV2(**meta_dict) img = Image.open(args.inp) muddler = ImageMuddlerGilbert( tile_h=meta.tile_size[0], tile_w=meta.tile_size[1], stride_y=meta.stride[0], stride_x=meta.stride[1], key=0, # decode 不依赖 key pad_mode=meta.pad_mode, ) out_img = muddler.decode(img, meta) out_img.save(args.out) print("完成:", args.out) if __name__ == "__main__": main()

新人小小水个帖子~

f4f4454a36e8c6d6ed9426f1dec7b7f91440×1062 196 KB

标签:算法纯水
问题描述:

有时候在一些聊天分享的社区/群聊里面会看到类似的图像

image1020×618 107 KB

这种看上去纯乱码的图片是什么呢?

这其实是一种利用了 Gilbert 曲线(广义希尔伯特曲线) 的图像混淆(至于为啥要混淆那心照不宣)
e974199a120267e22f6f88a1a4dd2ef9602×624 41.7 KB

但是这样混淆掉的图像也有很明显的问题:别人压根不知道解开后里面会是啥内容,万一开到一些不好的把人吓出心理阴影咋办

基于此,我又在这个基础上加了一层小预览机制

  • 先把原图按 tile 切块
  • 按固定步长抽样一些 tile
  • 把这些 tile 重新拼到左上角
  • 形成一个低分辨率预览
  • 剩下区域再做 Gilbert 路径混淆

这样最后出来的图就很有意思:

  • 左上角还能大致看出原图内容
  • 主体区域已经被混淆

就像这样:

unmuddled1024×1024 377 KB
muddled1026×1026 532 KB

也可以调整的更抽象一些(细节更低、尺寸更小),这里为了方便看出来我调的比较清晰,实际上可用很抽象的:
image1491×1380 485 KB

Python的实现逻辑:

#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ image_muddle_gilbert.py CLI 示例: # 编码:保留左上角预览,非预览区使用 Gilbert 曲线路径做循环位移混肴 python image_muddle_gilbert.py encode --in in.jpg --out muddled.png \ --tile 32 32 --stride 4 4 --key 123456 --embed-meta # 解码(若 PNG 已嵌入元数据) python image_muddle_gilbert.py decode --in muddled.png --out restored.png # 或解码时显式提供 JSON 元数据 python image_muddle_gilbert.py decode --in muddled.png --out restored.png --meta meta.json """ from __future__ import annotations import argparse import json import math import random from dataclasses import dataclass, asdict from typing import List, Tuple, Dict, Any import numpy as np from PIL import Image, PngImagePlugin def _sign(x: int) -> int: return (x > 0) - (x < 0) def _generate2d(x: int, y: int, ax: int, ay: int, bx: int, by: int, coords: List[Tuple[int, int]]) -> None: w = abs(ax + ay) h = abs(bx + by) dax, day = _sign(ax), _sign(ay) # 主方向的单位步长 dbx, dby = _sign(bx), _sign(by) # 正交方向的单位步长 if h == 1: # 一行填充 for _ in range(w): coords.append((x, y)) x += dax y += day return if w == 1: # 一列填充 for _ in range(h): coords.append((x, y)) x += dbx y += dby return ax2, ay2 = ax // 2, ay // 2 bx2, by2 = bx // 2, by // 2 w2 = abs(ax2 + ay2) h2 = abs(bx2 + by2) if 2 * w > 3 * h: if (w2 % 2) and (w > 2): # 偏好偶数步长 ax2 += dax ay2 += day # “长”情形:分两段 _generate2d(x, y, ax2, ay2, bx, by, coords) _generate2d(x + ax2, y + ay2, ax - ax2, ay - ay2, bx, by, coords) else: if (h2 % 2) and (h > 2): # 偏好偶数步长 bx2 += dbx by2 += dby # 标准情形:上一步 + 长水平 + 下一步 _generate2d(x, y, bx2, by2, ax2, ay2, coords) _generate2d(x + bx2, y + by2, ax, ay, bx - bx2, by - by2, coords) _generate2d(x + (ax - dax) + (bx2 - dbx), y + (ay - day) + (by2 - dby), -bx2, -by2, -(ax - ax2), -(ay - ay2), coords) def gilbert2d(width: int, height: int) -> List[Tuple[int, int]]: """返回按广义 Hilbert 曲线遍历 width×height 矩形的像素坐标序列 (x,y)。""" coordinates: List[Tuple[int, int]] = [] if width >= height: _generate2d(0, 0, width, 0, 0, height, coordinates) else: _generate2d(0, 0, 0, height, width, 0, coordinates) return coordinates @dataclass class MuddleMetaV2: version: int # 2 algo: str # 'gilbert_shift' orig_size: Tuple[int, int] # (H, W) padded_size: Tuple[int, int] # (Hp, Wp) mode: str # 原图模式(如 'RGB') tile_size: Tuple[int, int] # (tile_h, tile_w) grid_size: Tuple[int, int] # (R, C) stride: Tuple[int, int] # (stride_y, stride_x) preview_grid_size: Tuple[int, int] # (Pr, Pc) pad_mode: str # 预览抽样结果(原 tile 坐标) preview_tiles: List[Tuple[int, int]] # Gilbert 路径的“循环位移”参数;这里只记录 offset 与 M,便于分析/互操作 perm: Dict[str, int] # {'offset': O, 'M': M} notes: str # 说明 def _to_array(img: Image.Image) -> np.ndarray: """PIL Image -> (H, W, 4) RGBA uint8""" if img.mode != "RGBA": img = img.convert("RGBA") return np.array(img, dtype=np.uint8) def _pad_to_multiple(arr: np.ndarray, th: int, tw: int, pad_mode: str) -> Tuple[int, int, np.ndarray]: """把图像 pad 成 tile 的整数倍尺寸。""" H, W, C = arr.shape ph = (th - (H % th)) % th pw = (tw - (W % tw)) % tw if ph == 0 and pw == 0: return H, W, arr if pad_mode == "edge": arr2 = np.pad(arr, ((0, ph), (0, pw), (0, 0)), mode="edge") elif pad_mode == "constant": arr2 = np.pad(arr, ((0, ph), (0, pw), (0, 0)), mode="constant", constant_values=0) else: raise ValueError(f"未知 pad_mode: {pad_mode}") return H + ph, W + pw, arr2 def _copy_tile(src: np.ndarray, dst: np.ndarray, src_tile: Tuple[int, int], dst_tile: Tuple[int, int], tile_size: Tuple[int, int]) -> None: """以 tile 为单位拷贝(按像素)""" th, tw = tile_size sr, sc = src_tile dr, dc = dst_tile sy0, sy1 = sr * th, (sr + 1) * th sx0, sx1 = sc * tw, (sc + 1) * tw dy0, dy1 = dr * th, (dr + 1) * th dx0, dx1 = dc * tw, (dc + 1) * tw dst[dy0:dy1, dx0:dx1, :] = src[sy0:sy1, sx0:sx1, :] def _deriv_offset(M: int, key: int) -> int: """ 从 key 推导循环位移 offset。 参考小番茄实现:offset ≈ round(phi * (W*H)),phi = (sqrt(5)-1)/2。 这里在此基础上加上 key 的扰动,避免 0 与可预测性。 """ if M <= 1: return 0 phi = (math.sqrt(5.0) - 1.0) / 2.0 base = int(round(phi * M)) # 线性同余扰动,让不同 key 有不同 offset;确保落在 [1, M-1] rnd = (1103515245 * (key & 0xFFFFFFFF) + 12345) & 0x7FFFFFFF offset = (base + rnd) % M if offset == 0: offset = 1 return offset class ImageMuddlerGilbert: def __init__( self, tile_h: int = 32, tile_w: int = 32, stride_y: int = 4, stride_x: int = 4, key: int = 123456, pad_mode: str = "edge", ): assert tile_h > 0 and tile_w > 0 assert stride_y > 0 and stride_x > 0 self.tile_h = tile_h self.tile_w = tile_w self.stride_y = stride_y self.stride_x = stride_x self.key = key self.pad_mode = pad_mode def encode(self, img: Image.Image) -> Tuple[Image.Image, MuddleMetaV2]: mode = img.mode arr = _to_array(img) H, W, _ = arr.shape Hp, Wp, arr_padded = _pad_to_multiple(arr, self.tile_h, self.tile_w, self.pad_mode) R = Hp // self.tile_h Cc = Wp // self.tile_w # 预览 tile:按 stride 抽样 preview_tiles = [(r, c) for r in range(0, R, self.stride_y) for c in range(0, Cc, self.stride_x)] Pr = len(range(0, R, self.stride_y)) Pc = len(range(0, Cc, self.stride_x)) out = np.zeros_like(arr_padded) # 1) 拷贝预览:将 preview_tiles(原图位置)复制到输出左上角的 Pr×Pc 个 tile for idx, (tr, tc) in enumerate(preview_tiles): pr = idx // Pc pc = idx % Pc _copy_tile(arr_padded, out, src_tile=(tr, tc), dst_tile=(pr, pc), tile_size=(self.tile_h, self.tile_w)) # 2) 生成两套 Gilbert 序列 is_preview_tile = np.zeros((R, Cc), dtype=bool) for tr, tc in preview_tiles: is_preview_tile[tr, tc] = True preH = Pr * self.tile_h preW = Pc * self.tile_w # 全图的 Gilbert 曲线(x,y) path_xy = gilbert2d(Wp, Hp) # (x, y) def in_preview_tile_xy(x: int, y: int) -> bool: tr = y // self.tile_h tc = x // self.tile_w return is_preview_tile[tr, tc] # source:排除“原始预览 tile” source_lin = [y * Wp + x for (x, y) in path_xy if not in_preview_tile_xy(x, y)] # dest :排除“左上角预览矩形” dest_lin = [y * Wp + x for (x, y) in path_xy if not (y < preH and x < preW)] if len(source_lin) != len(dest_lin): raise RuntimeError(f"像素数量不一致:source={len(source_lin)} vs dest={len(dest_lin)}") M = len(source_lin) if M > 0: offset = _deriv_offset(M, self.key) source_idx = np.asarray(source_lin, dtype=np.int64) dest_idx = np.asarray(dest_lin, dtype=np.int64) dest_rot = np.roll(dest_idx, -offset) # 对应 j = (i+offset) % M flat_in = arr_padded.reshape(-1, arr_padded.shape[2]) flat_out = out.reshape(-1, out.shape[2]) flat_out[dest_rot] = flat_in[source_idx] else: offset = 0 meta = MuddleMetaV2( version=2, algo="gilbert_shift", orig_size=(H, W), padded_size=(Hp, Wp), mode=mode, tile_size=(self.tile_h, self.tile_w), grid_size=(R, Cc), stride=(self.stride_y, self.stride_x), preview_grid_size=(Pr, Pc), pad_mode=self.pad_mode, preview_tiles=preview_tiles, perm={"offset": int(offset), "M": int(M)}, notes="source: 全图 Gilbert 排除原始预览 tile;dest: 全图 Gilbert 排除预览矩形;对齐顺序后做循环位移。" ) out_img = Image.fromarray(out, mode="RGBA").convert(mode) return out_img, meta def decode(self, muddled_img: Image.Image, meta: MuddleMetaV2) -> Image.Image: if meta.version != 2 or meta.algo != "gilbert_shift": raise ValueError("该元数据不是 gilbert_shift v2 格式,无法解码。") mode = meta.mode arr_in = _to_array(muddled_img) Hp, Wp = meta.padded_size if (arr_in.shape[0], arr_in.shape[1]) != (Hp, Wp): raise ValueError(f"输入混肴图尺寸 {arr_in.shape[:2]} 与元数据不符 {meta.padded_size}") out = np.zeros_like(arr_in) R, Cc = meta.grid_size tile_h, tile_w = meta.tile_size Pr, Pc = meta.preview_grid_size # 1) 还原预览:将左上角 Pr×Pc 个 tile 拷回原始预览 tile 的位置 preview_tiles = meta.preview_tiles for idx, (tr, tc) in enumerate(preview_tiles): pr = idx // Pc pc = idx % Pc _copy_tile(arr_in, out, src_tile=(pr, pc), dst_tile=(tr, tc), tile_size=(tile_h, tile_w)) # 2) 生成两套 Gilbert 序列(与 encode 完全一致) is_preview_tile = np.zeros((R, Cc), dtype=bool) for tr, tc in preview_tiles: is_preview_tile[tr, tc] = True preH = Pr * tile_h preW = Pc * tile_w path_xy = gilbert2d(Wp, Hp) def in_preview_tile_xy(x: int, y: int) -> bool: tr = y // tile_h tc = x // tile_w return is_preview_tile[tr, tc] source_lin = [y * Wp + x for (x, y) in path_xy if not in_preview_tile_xy(x, y)] dest_lin = [y * Wp + x for (x, y) in path_xy if not (y < preH and x < preW)] M = len(source_lin) if M != meta.perm["M"] or M != len(dest_lin): raise ValueError("元数据 M 与当前尺寸/参数不一致,无法解码。") offset = int(meta.perm["offset"]) if M > 0 else 0 if M > 0: source_idx = np.asarray(source_lin, dtype=np.int64) dest_idx = np.asarray(dest_lin, dtype=np.int64) dest_rot = np.roll(dest_idx, -offset) # encode 写入位置 flat_in = arr_in.reshape(-1, arr_in.shape[2]) flat_out = out.reshape(-1, out.shape[2]) # 解码:把 in[dest_rot] 放回 out[source_idx] flat_out[source_idx] = flat_in[dest_rot] # 裁剪回原尺寸 H, W = meta.orig_size out = out[:H, :W, :] return Image.fromarray(out, mode="RGBA").convert(mode) PNG_TEXT_KEY = "muddle_meta" def save_with_meta(img: Image.Image, out_path: str, meta: MuddleMetaV2, embed: bool) -> None: if embed: if not out_path.lower().endswith(".png"): raise ValueError("只有 PNG 支持嵌入元数据,请将输出文件扩展名设为 .png 或关闭 --embed-meta") pnginfo = PngImagePlugin.PngInfo() pnginfo.add_text(PNG_TEXT_KEY, json.dumps(asdict(meta), ensure_ascii=False)) img.save(out_path, pnginfo=pnginfo) else: img.save(out_path) def try_load_meta_from_png(png_path: str) -> Dict[str, Any] | None: im = Image.open(png_path) if hasattr(im, "text") and PNG_TEXT_KEY in im.text: raw = im.text[PNG_TEXT_KEY] return json.loads(raw) return None def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( description="图像混肴(可逆):左上角预览 + Gilbert 曲线路径循环位移;支持 encode/decode", formatter_class=argparse.ArgumentDefaultsHelpFormatter ) subp = p.add_subparsers(dest="cmd", required=True) pe = subp.add_parser("encode", help="混肴图像") pe.add_argument("--in", dest="inp", required=True, help="输入图像路径") pe.add_argument("--out", dest="out", required=True, help="输出混肴图路径(PNG/JPG 等)") pe.add_argument("--meta", dest="meta", default=None, help="元数据 JSON 输出路径(若 --embed-meta 则可不写)") pe.add_argument("--embed-meta", action="store_true", help="把元数据嵌入 PNG(tEXt)") pe.add_argument("--tile", nargs=2, type=int, default=[32, 32], metavar=("H", "W"), help="tile 像素尺寸") pe.add_argument("--stride", nargs=2, type=int, default=[4, 4], metavar=("Y", "X"), help="预览抽样步长(按 tile)") pe.add_argument("--key", type=int, default=123456, help="位移密钥(影响 offset )") pe.add_argument("--pad-mode", choices=["edge", "constant"], default="edge", help="边界填充方式") pd = subp.add_parser("decode", help="还原混肴图像") pd.add_argument("--in", dest="inp", required=True, help="输入混肴图(PNG/JPG 等)") pd.add_argument("--out", dest="out", required=True, help="输出还原图路径") pd.add_argument("--meta", dest="meta", default=None, help="元数据 JSON 路径(若 PNG 内已嵌入则可省略)") p.add_argument("--selftest", action="store_true", help="运行一个自检:随机图像 encode->decode 完整性检查") return p def main(): parser = build_parser() args = parser.parse_args() if args.selftest: # 自检:随机图像往返测试 H, W = 64, 96 rnd = np.random.default_rng(42) arr = (rnd.integers(0, 256, size=(H, W, 3), dtype=np.uint8)) img = Image.fromarray(arr, mode="RGB") muddler = ImageMuddlerGilbert( tile_h=16, tile_w=16, stride_y=4, stride_x=4, key=2024, pad_mode="edge" ) enc_img, meta = muddler.encode(img) dec_img = muddler.decode(enc_img, meta) ok = np.array_equal(np.array(dec_img), np.array(img)) print("SELFTEST:", "PASS" if ok else "FAIL") return if args.cmd == "encode": img = Image.open(args.inp) tile_h, tile_w = args.tile stride_y, stride_x = args.stride muddler = ImageMuddlerGilbert( tile_h=tile_h, tile_w=tile_w, stride_y=stride_y, stride_x=stride_x, key=args.key, pad_mode=args.pad_mode, ) out_img, meta = muddler.encode(img) # 保存图像 save_with_meta(out_img, args.out, meta, embed=args.embed_meta) # 保存/输出元数据 if args.meta: with open(args.meta, "w", encoding="utf-8") as f: json.dump(asdict(meta), f, ensure_ascii=False, indent=2) elif not args.embed_meta: print("⚠ 未保存元数据:建议使用 --meta 输出 JSON 或使用 --embed-meta 嵌入 PNG") else: print("✅ 元数据已嵌入 PNG tEXt 块") print("完成:", args.out) elif args.cmd == "decode": # 先尝试从 PNG 里读元数据 meta_dict = None if args.meta is None and args.inp.lower().endswith(".png"): meta_dict = try_load_meta_from_png(args.inp) if meta_dict is not None: print("🔎 已从 PNG 读取嵌入的元数据") if meta_dict is None: if args.meta is None: raise SystemExit("需要元数据:请提供 --meta JSON,或输入 PNG 内需已嵌入元数据") with open(args.meta, "r", encoding="utf-8") as f: meta_dict = json.load(f) if meta_dict.get("version") != 2 or meta_dict.get("algo") != "gilbert_shift": raise SystemExit("该元数据不是 gilbert_shift v2 格式,无法解码。") meta = MuddleMetaV2(**meta_dict) img = Image.open(args.inp) muddler = ImageMuddlerGilbert( tile_h=meta.tile_size[0], tile_w=meta.tile_size[1], stride_y=meta.stride[0], stride_x=meta.stride[1], key=0, # decode 不依赖 key pad_mode=meta.pad_mode, ) out_img = muddler.decode(img, meta) out_img.save(args.out) print("完成:", args.out) if __name__ == "__main__": main()

新人小小水个帖子~

f4f4454a36e8c6d6ed9426f1dec7b7f91440×1062 196 KB

网友解答:
--【壹】--:

有时候在一些聊天分享的社区/群聊里面会看到类似的图像

image1020×618 107 KB

这种看上去纯乱码的图片是什么呢?

这其实是一种利用了 Gilbert 曲线(广义希尔伯特曲线) 的图像混淆(至于为啥要混淆那心照不宣)
e974199a120267e22f6f88a1a4dd2ef9602×624 41.7 KB

但是这样混淆掉的图像也有很明显的问题:别人压根不知道解开后里面会是啥内容,万一开到一些不好的把人吓出心理阴影咋办

基于此,我又在这个基础上加了一层小预览机制

  • 先把原图按 tile 切块
  • 按固定步长抽样一些 tile
  • 把这些 tile 重新拼到左上角
  • 形成一个低分辨率预览
  • 剩下区域再做 Gilbert 路径混淆

这样最后出来的图就很有意思:

  • 左上角还能大致看出原图内容
  • 主体区域已经被混淆

就像这样:

unmuddled1024×1024 377 KB
muddled1026×1026 532 KB

也可以调整的更抽象一些(细节更低、尺寸更小),这里为了方便看出来我调的比较清晰,实际上可用很抽象的:
image1491×1380 485 KB

Python的实现逻辑:

#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ image_muddle_gilbert.py CLI 示例: # 编码:保留左上角预览,非预览区使用 Gilbert 曲线路径做循环位移混肴 python image_muddle_gilbert.py encode --in in.jpg --out muddled.png \ --tile 32 32 --stride 4 4 --key 123456 --embed-meta # 解码(若 PNG 已嵌入元数据) python image_muddle_gilbert.py decode --in muddled.png --out restored.png # 或解码时显式提供 JSON 元数据 python image_muddle_gilbert.py decode --in muddled.png --out restored.png --meta meta.json """ from __future__ import annotations import argparse import json import math import random from dataclasses import dataclass, asdict from typing import List, Tuple, Dict, Any import numpy as np from PIL import Image, PngImagePlugin def _sign(x: int) -> int: return (x > 0) - (x < 0) def _generate2d(x: int, y: int, ax: int, ay: int, bx: int, by: int, coords: List[Tuple[int, int]]) -> None: w = abs(ax + ay) h = abs(bx + by) dax, day = _sign(ax), _sign(ay) # 主方向的单位步长 dbx, dby = _sign(bx), _sign(by) # 正交方向的单位步长 if h == 1: # 一行填充 for _ in range(w): coords.append((x, y)) x += dax y += day return if w == 1: # 一列填充 for _ in range(h): coords.append((x, y)) x += dbx y += dby return ax2, ay2 = ax // 2, ay // 2 bx2, by2 = bx // 2, by // 2 w2 = abs(ax2 + ay2) h2 = abs(bx2 + by2) if 2 * w > 3 * h: if (w2 % 2) and (w > 2): # 偏好偶数步长 ax2 += dax ay2 += day # “长”情形:分两段 _generate2d(x, y, ax2, ay2, bx, by, coords) _generate2d(x + ax2, y + ay2, ax - ax2, ay - ay2, bx, by, coords) else: if (h2 % 2) and (h > 2): # 偏好偶数步长 bx2 += dbx by2 += dby # 标准情形:上一步 + 长水平 + 下一步 _generate2d(x, y, bx2, by2, ax2, ay2, coords) _generate2d(x + bx2, y + by2, ax, ay, bx - bx2, by - by2, coords) _generate2d(x + (ax - dax) + (bx2 - dbx), y + (ay - day) + (by2 - dby), -bx2, -by2, -(ax - ax2), -(ay - ay2), coords) def gilbert2d(width: int, height: int) -> List[Tuple[int, int]]: """返回按广义 Hilbert 曲线遍历 width×height 矩形的像素坐标序列 (x,y)。""" coordinates: List[Tuple[int, int]] = [] if width >= height: _generate2d(0, 0, width, 0, 0, height, coordinates) else: _generate2d(0, 0, 0, height, width, 0, coordinates) return coordinates @dataclass class MuddleMetaV2: version: int # 2 algo: str # 'gilbert_shift' orig_size: Tuple[int, int] # (H, W) padded_size: Tuple[int, int] # (Hp, Wp) mode: str # 原图模式(如 'RGB') tile_size: Tuple[int, int] # (tile_h, tile_w) grid_size: Tuple[int, int] # (R, C) stride: Tuple[int, int] # (stride_y, stride_x) preview_grid_size: Tuple[int, int] # (Pr, Pc) pad_mode: str # 预览抽样结果(原 tile 坐标) preview_tiles: List[Tuple[int, int]] # Gilbert 路径的“循环位移”参数;这里只记录 offset 与 M,便于分析/互操作 perm: Dict[str, int] # {'offset': O, 'M': M} notes: str # 说明 def _to_array(img: Image.Image) -> np.ndarray: """PIL Image -> (H, W, 4) RGBA uint8""" if img.mode != "RGBA": img = img.convert("RGBA") return np.array(img, dtype=np.uint8) def _pad_to_multiple(arr: np.ndarray, th: int, tw: int, pad_mode: str) -> Tuple[int, int, np.ndarray]: """把图像 pad 成 tile 的整数倍尺寸。""" H, W, C = arr.shape ph = (th - (H % th)) % th pw = (tw - (W % tw)) % tw if ph == 0 and pw == 0: return H, W, arr if pad_mode == "edge": arr2 = np.pad(arr, ((0, ph), (0, pw), (0, 0)), mode="edge") elif pad_mode == "constant": arr2 = np.pad(arr, ((0, ph), (0, pw), (0, 0)), mode="constant", constant_values=0) else: raise ValueError(f"未知 pad_mode: {pad_mode}") return H + ph, W + pw, arr2 def _copy_tile(src: np.ndarray, dst: np.ndarray, src_tile: Tuple[int, int], dst_tile: Tuple[int, int], tile_size: Tuple[int, int]) -> None: """以 tile 为单位拷贝(按像素)""" th, tw = tile_size sr, sc = src_tile dr, dc = dst_tile sy0, sy1 = sr * th, (sr + 1) * th sx0, sx1 = sc * tw, (sc + 1) * tw dy0, dy1 = dr * th, (dr + 1) * th dx0, dx1 = dc * tw, (dc + 1) * tw dst[dy0:dy1, dx0:dx1, :] = src[sy0:sy1, sx0:sx1, :] def _deriv_offset(M: int, key: int) -> int: """ 从 key 推导循环位移 offset。 参考小番茄实现:offset ≈ round(phi * (W*H)),phi = (sqrt(5)-1)/2。 这里在此基础上加上 key 的扰动,避免 0 与可预测性。 """ if M <= 1: return 0 phi = (math.sqrt(5.0) - 1.0) / 2.0 base = int(round(phi * M)) # 线性同余扰动,让不同 key 有不同 offset;确保落在 [1, M-1] rnd = (1103515245 * (key & 0xFFFFFFFF) + 12345) & 0x7FFFFFFF offset = (base + rnd) % M if offset == 0: offset = 1 return offset class ImageMuddlerGilbert: def __init__( self, tile_h: int = 32, tile_w: int = 32, stride_y: int = 4, stride_x: int = 4, key: int = 123456, pad_mode: str = "edge", ): assert tile_h > 0 and tile_w > 0 assert stride_y > 0 and stride_x > 0 self.tile_h = tile_h self.tile_w = tile_w self.stride_y = stride_y self.stride_x = stride_x self.key = key self.pad_mode = pad_mode def encode(self, img: Image.Image) -> Tuple[Image.Image, MuddleMetaV2]: mode = img.mode arr = _to_array(img) H, W, _ = arr.shape Hp, Wp, arr_padded = _pad_to_multiple(arr, self.tile_h, self.tile_w, self.pad_mode) R = Hp // self.tile_h Cc = Wp // self.tile_w # 预览 tile:按 stride 抽样 preview_tiles = [(r, c) for r in range(0, R, self.stride_y) for c in range(0, Cc, self.stride_x)] Pr = len(range(0, R, self.stride_y)) Pc = len(range(0, Cc, self.stride_x)) out = np.zeros_like(arr_padded) # 1) 拷贝预览:将 preview_tiles(原图位置)复制到输出左上角的 Pr×Pc 个 tile for idx, (tr, tc) in enumerate(preview_tiles): pr = idx // Pc pc = idx % Pc _copy_tile(arr_padded, out, src_tile=(tr, tc), dst_tile=(pr, pc), tile_size=(self.tile_h, self.tile_w)) # 2) 生成两套 Gilbert 序列 is_preview_tile = np.zeros((R, Cc), dtype=bool) for tr, tc in preview_tiles: is_preview_tile[tr, tc] = True preH = Pr * self.tile_h preW = Pc * self.tile_w # 全图的 Gilbert 曲线(x,y) path_xy = gilbert2d(Wp, Hp) # (x, y) def in_preview_tile_xy(x: int, y: int) -> bool: tr = y // self.tile_h tc = x // self.tile_w return is_preview_tile[tr, tc] # source:排除“原始预览 tile” source_lin = [y * Wp + x for (x, y) in path_xy if not in_preview_tile_xy(x, y)] # dest :排除“左上角预览矩形” dest_lin = [y * Wp + x for (x, y) in path_xy if not (y < preH and x < preW)] if len(source_lin) != len(dest_lin): raise RuntimeError(f"像素数量不一致:source={len(source_lin)} vs dest={len(dest_lin)}") M = len(source_lin) if M > 0: offset = _deriv_offset(M, self.key) source_idx = np.asarray(source_lin, dtype=np.int64) dest_idx = np.asarray(dest_lin, dtype=np.int64) dest_rot = np.roll(dest_idx, -offset) # 对应 j = (i+offset) % M flat_in = arr_padded.reshape(-1, arr_padded.shape[2]) flat_out = out.reshape(-1, out.shape[2]) flat_out[dest_rot] = flat_in[source_idx] else: offset = 0 meta = MuddleMetaV2( version=2, algo="gilbert_shift", orig_size=(H, W), padded_size=(Hp, Wp), mode=mode, tile_size=(self.tile_h, self.tile_w), grid_size=(R, Cc), stride=(self.stride_y, self.stride_x), preview_grid_size=(Pr, Pc), pad_mode=self.pad_mode, preview_tiles=preview_tiles, perm={"offset": int(offset), "M": int(M)}, notes="source: 全图 Gilbert 排除原始预览 tile;dest: 全图 Gilbert 排除预览矩形;对齐顺序后做循环位移。" ) out_img = Image.fromarray(out, mode="RGBA").convert(mode) return out_img, meta def decode(self, muddled_img: Image.Image, meta: MuddleMetaV2) -> Image.Image: if meta.version != 2 or meta.algo != "gilbert_shift": raise ValueError("该元数据不是 gilbert_shift v2 格式,无法解码。") mode = meta.mode arr_in = _to_array(muddled_img) Hp, Wp = meta.padded_size if (arr_in.shape[0], arr_in.shape[1]) != (Hp, Wp): raise ValueError(f"输入混肴图尺寸 {arr_in.shape[:2]} 与元数据不符 {meta.padded_size}") out = np.zeros_like(arr_in) R, Cc = meta.grid_size tile_h, tile_w = meta.tile_size Pr, Pc = meta.preview_grid_size # 1) 还原预览:将左上角 Pr×Pc 个 tile 拷回原始预览 tile 的位置 preview_tiles = meta.preview_tiles for idx, (tr, tc) in enumerate(preview_tiles): pr = idx // Pc pc = idx % Pc _copy_tile(arr_in, out, src_tile=(pr, pc), dst_tile=(tr, tc), tile_size=(tile_h, tile_w)) # 2) 生成两套 Gilbert 序列(与 encode 完全一致) is_preview_tile = np.zeros((R, Cc), dtype=bool) for tr, tc in preview_tiles: is_preview_tile[tr, tc] = True preH = Pr * tile_h preW = Pc * tile_w path_xy = gilbert2d(Wp, Hp) def in_preview_tile_xy(x: int, y: int) -> bool: tr = y // tile_h tc = x // tile_w return is_preview_tile[tr, tc] source_lin = [y * Wp + x for (x, y) in path_xy if not in_preview_tile_xy(x, y)] dest_lin = [y * Wp + x for (x, y) in path_xy if not (y < preH and x < preW)] M = len(source_lin) if M != meta.perm["M"] or M != len(dest_lin): raise ValueError("元数据 M 与当前尺寸/参数不一致,无法解码。") offset = int(meta.perm["offset"]) if M > 0 else 0 if M > 0: source_idx = np.asarray(source_lin, dtype=np.int64) dest_idx = np.asarray(dest_lin, dtype=np.int64) dest_rot = np.roll(dest_idx, -offset) # encode 写入位置 flat_in = arr_in.reshape(-1, arr_in.shape[2]) flat_out = out.reshape(-1, out.shape[2]) # 解码:把 in[dest_rot] 放回 out[source_idx] flat_out[source_idx] = flat_in[dest_rot] # 裁剪回原尺寸 H, W = meta.orig_size out = out[:H, :W, :] return Image.fromarray(out, mode="RGBA").convert(mode) PNG_TEXT_KEY = "muddle_meta" def save_with_meta(img: Image.Image, out_path: str, meta: MuddleMetaV2, embed: bool) -> None: if embed: if not out_path.lower().endswith(".png"): raise ValueError("只有 PNG 支持嵌入元数据,请将输出文件扩展名设为 .png 或关闭 --embed-meta") pnginfo = PngImagePlugin.PngInfo() pnginfo.add_text(PNG_TEXT_KEY, json.dumps(asdict(meta), ensure_ascii=False)) img.save(out_path, pnginfo=pnginfo) else: img.save(out_path) def try_load_meta_from_png(png_path: str) -> Dict[str, Any] | None: im = Image.open(png_path) if hasattr(im, "text") and PNG_TEXT_KEY in im.text: raw = im.text[PNG_TEXT_KEY] return json.loads(raw) return None def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( description="图像混肴(可逆):左上角预览 + Gilbert 曲线路径循环位移;支持 encode/decode", formatter_class=argparse.ArgumentDefaultsHelpFormatter ) subp = p.add_subparsers(dest="cmd", required=True) pe = subp.add_parser("encode", help="混肴图像") pe.add_argument("--in", dest="inp", required=True, help="输入图像路径") pe.add_argument("--out", dest="out", required=True, help="输出混肴图路径(PNG/JPG 等)") pe.add_argument("--meta", dest="meta", default=None, help="元数据 JSON 输出路径(若 --embed-meta 则可不写)") pe.add_argument("--embed-meta", action="store_true", help="把元数据嵌入 PNG(tEXt)") pe.add_argument("--tile", nargs=2, type=int, default=[32, 32], metavar=("H", "W"), help="tile 像素尺寸") pe.add_argument("--stride", nargs=2, type=int, default=[4, 4], metavar=("Y", "X"), help="预览抽样步长(按 tile)") pe.add_argument("--key", type=int, default=123456, help="位移密钥(影响 offset )") pe.add_argument("--pad-mode", choices=["edge", "constant"], default="edge", help="边界填充方式") pd = subp.add_parser("decode", help="还原混肴图像") pd.add_argument("--in", dest="inp", required=True, help="输入混肴图(PNG/JPG 等)") pd.add_argument("--out", dest="out", required=True, help="输出还原图路径") pd.add_argument("--meta", dest="meta", default=None, help="元数据 JSON 路径(若 PNG 内已嵌入则可省略)") p.add_argument("--selftest", action="store_true", help="运行一个自检:随机图像 encode->decode 完整性检查") return p def main(): parser = build_parser() args = parser.parse_args() if args.selftest: # 自检:随机图像往返测试 H, W = 64, 96 rnd = np.random.default_rng(42) arr = (rnd.integers(0, 256, size=(H, W, 3), dtype=np.uint8)) img = Image.fromarray(arr, mode="RGB") muddler = ImageMuddlerGilbert( tile_h=16, tile_w=16, stride_y=4, stride_x=4, key=2024, pad_mode="edge" ) enc_img, meta = muddler.encode(img) dec_img = muddler.decode(enc_img, meta) ok = np.array_equal(np.array(dec_img), np.array(img)) print("SELFTEST:", "PASS" if ok else "FAIL") return if args.cmd == "encode": img = Image.open(args.inp) tile_h, tile_w = args.tile stride_y, stride_x = args.stride muddler = ImageMuddlerGilbert( tile_h=tile_h, tile_w=tile_w, stride_y=stride_y, stride_x=stride_x, key=args.key, pad_mode=args.pad_mode, ) out_img, meta = muddler.encode(img) # 保存图像 save_with_meta(out_img, args.out, meta, embed=args.embed_meta) # 保存/输出元数据 if args.meta: with open(args.meta, "w", encoding="utf-8") as f: json.dump(asdict(meta), f, ensure_ascii=False, indent=2) elif not args.embed_meta: print("⚠ 未保存元数据:建议使用 --meta 输出 JSON 或使用 --embed-meta 嵌入 PNG") else: print("✅ 元数据已嵌入 PNG tEXt 块") print("完成:", args.out) elif args.cmd == "decode": # 先尝试从 PNG 里读元数据 meta_dict = None if args.meta is None and args.inp.lower().endswith(".png"): meta_dict = try_load_meta_from_png(args.inp) if meta_dict is not None: print("🔎 已从 PNG 读取嵌入的元数据") if meta_dict is None: if args.meta is None: raise SystemExit("需要元数据:请提供 --meta JSON,或输入 PNG 内需已嵌入元数据") with open(args.meta, "r", encoding="utf-8") as f: meta_dict = json.load(f) if meta_dict.get("version") != 2 or meta_dict.get("algo") != "gilbert_shift": raise SystemExit("该元数据不是 gilbert_shift v2 格式,无法解码。") meta = MuddleMetaV2(**meta_dict) img = Image.open(args.inp) muddler = ImageMuddlerGilbert( tile_h=meta.tile_size[0], tile_w=meta.tile_size[1], stride_y=meta.stride[0], stride_x=meta.stride[1], key=0, # decode 不依赖 key pad_mode=meta.pad_mode, ) out_img = muddler.decode(img, meta) out_img.save(args.out) print("完成:", args.out) if __name__ == "__main__": main()

新人小小水个帖子~

f4f4454a36e8c6d6ed9426f1dec7b7f91440×1062 196 KB

标签:算法纯水