在一些社群看到的“乱码”图是啥,以及针对的一点改进
- 内容介绍
- 文章标签
- 相关推荐
有时候在一些聊天分享的社区/群聊里面会看到类似的图像
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

