棋盘格隐写:基于奇偶相位的双图隐藏与还原

摘要:本文介绍一种基于棋盘格奇偶相位的图像隐写方法:将原图映射到高亮度区间,将暗影图映射到低亮度区间,再按棋盘格交替写入同一张图。解码时分别提取奇相位和偶相位像素 ,即可还原原图与暗影图。文中包含编码、检测、解码的完整 Python 实现,并支持反色、灰度、边缘、模糊等暗影生成方式。

关键字:图像隐写,PIL,NumPy,图像处理,信息安全CTF,棋盘格隐写,奇偶相位

前沿

在信息安全ctf比赛中,misc方向是必考的一个方向,其中,图片隐写也是最常见的题目类型。我们介绍棋盘格奇偶隐写。其中利用像素的奇偶位置和亮度区间,就能把两张图同时藏进一张图里。解码时分别提取奇相位和偶相位像素,就能还原出原图和一张"暗影图"。

这篇文章会从原理到代码,完整讲解如何用 Python + PIL + NumPy 实现这套方案,包括:编码:把原图和暗影图合成一张棋盘格隐写图;检测:判断一张图是否使用了这种隐写;解码:从隐写图中还原出原图和暗影图。代码分为两个文件包括编码、解码、检测以及还原。

一、效果预览

我们有一张原图对原图进行编码 :生成一张尺寸为原图两倍的隐写图 stego_checkerboard.png。这张图看起来像原图和暗影图的棋盘格交错。解码 :从隐写图中还原出 decoded_origin.png 和 decoded_shadow.png。

如果暗影模式选择 invert(反色),那么解码后会得到原图和它的反色图;如果选择 gray,则得到原图和灰度图;edges 得到边缘图;blur 得到模糊图。

二、核心原理

1. 棋盘格与奇偶相位

把一张图的像素按坐标 (x, y) 分成两类:

  • 偶相位 :(x + y) % 2 == 0

  • 奇相位 :(x + y) % 2 == 1

在棋盘格中,这两类像素交替出现,就像国际象棋棋盘的黑白格。

如果我们把原图 只写入奇相位,把暗影图只写入偶相位,那么每个像素位置只承载一张图的信息,互不干扰。解码时按同样的规则提取即可。

2. 暗影图的生成方式

暗影图可以外部提供,也可以由原图自动生成。代码支持四种模式:

模式 说明
invert 反色图
gray 灰度图(转 L 再转回 RGB)
edges 边缘图(FIND_EDGES 滤波)
blur 高斯模糊图

这些模式生成的暗影图与原图差异明显,解码后能直观看到两张图。

编码解码执行案例:

原图: 暗影图:

三、编码实现:把两张图合成一张

代码执行中的 encode 函数负责编码。

1. 生成暗影图

复制代码
def generate_shadow(img_ori: Image.Image, mode: str = "invert") -> Image.Image:
    img_ori = img_ori.convert("RGB")
    if mode == "invert":
        return ImageOps.invert(img_ori)
    elif mode == "gray":
        return img_ori.convert("L").convert("RGB")
    elif mode == "edges":
        return img_ori.filter(ImageFilter.FIND_EDGES).convert("RGB")
    elif mode == "blur":
        return img_ori.filter(ImageFilter.GaussianBlur(radius=2))
    else:
        raise ValueError(f"不支持的 shadow_mode: {mode}")

如果 shadow_path 为 None,则调用这个函数自动生成;否则读取外部暗影图,并缩放到原图尺寸。

2. 色彩空间映射

复制代码
arr_ori = np.array(img_ori).astype(np.float32)
arr_sha = np.array(img_sha).astype(np.float32)

arr_sha_adj = (arr_sha / 255.0) * 127.0
arr_ori_adj = (arr_ori / 255.0) * 127.0 + 128.0

这样暗影值落在 [0, 127],原图值落在 [128, 255]。

3. 棋盘格填充

创建画布,然后按棋盘格规则写入:

复制代码
canvas = np.zeros((2 * H, 2 * W, 3), dtype=np.uint8)

​​​​​​​# 原图(奇相位)
canvas[1::2, 0::2] = np.round(arr_ori_adj).astype(np.uint8)
canvas[0::2, 1::2] = np.round(arr_ori_adj).astype(np.uint8)

# 暗影(偶相位)
canvas[0::2, 0::2] = np.round(arr_sha_adj).astype(np.uint8)
canvas[1::2, 1::2] = np.round(arr_sha_adj).astype(np.uint8)

注意:

  • canvas[1::2, 0::2]:行索引为奇数、列索引为偶数,(x+y) 为奇数 → 奇相位 → 原图
  • canvas[0::2, 1::2]:行偶列奇,(x+y) 奇数 → 奇相位 → 原图
  • canvas[0::2, 0::2]:行偶列偶,(x+y) 偶数 → 偶相位 → 暗影
  • canvas[1::2, 1::2]:行奇列奇,(x+y) 偶数 → 偶相位 → 暗影

每个 2×2 块中,两个奇相位像素放原图,两个偶相位像素放暗影。这样隐写图在视觉上呈现棋盘格交错。

最后保存为 output_path,尺寸变为 2W × 2H。


四、检测:判断是否为棋盘格隐写

代码执行中的 detect 函数用于检测。

1. 生成偶相位掩码

复制代码
def _even_mask(h, w):
    yy, xx = np.mgrid[0:h, 0:w]
    return (xx + yy) % 2 == 0

对于隐写图,尺寸是 2H × 2W。偶相位包括 (0,0), (0,2), (1,1), (1,3)... 等位置。

2. 统计值域

复制代码
a = np.array(Image.open(src).convert("RGB")).astype(np.int16)
h, w, _ = a.shape
even = _even_mask(h, w)

e, o = a[even], a[~even]

print(f"偶相位值域 {e.min()} ~ {e.max()}   均值 {e.mean():.1f}")
print(f"奇相位值域 {o.min()} ~ {o.max()}   均值 {o.mean():.1f}")

在编码时,偶相位是暗影(值域 [0, 127]),奇相位是原图(值域 [128, 255])。因此正常情况下:e.max() < o.min();即偶相位最大值小于奇相位最小值。

如果两张图的值域有重叠,比如暗影图恰好有很亮的像素,或者原图有很暗的像素,映射后仍可能重叠。但编码时已经强制错开区间,所以只要没有被有损压缩或调色破坏,检测就能通过。


五、解码:还原原图与暗影图

执行代码中的 decode 函数负责还原。

1. 提取奇偶相位

复制代码
a = np.array(Image.open(src).convert("RGB"))
odd_adj = a[1::2, 0::2]   # 奇相位 -> 原图
even_adj = a[0::2, 0::2]  # 偶相位 -> 暗影

这里只取了 [1::2, 0::2] 和 [0::2, 0::2],因为编码时每个相位有两个位置,它们写入的值完全相同,取其中一个即可。这样得到的数组尺寸是 H × W × 3,正好是原图尺寸。

2. 逆映射

复制代码
odd = np.clip((odd_adj - 128.0) / 127.0 * 255.0, 0, 255).astype(np.uint8)
even = np.clip(even_adj / 127.0 * 255.0, 0, 255).astype(np.uint8)
  • 原图:先减 128,再除以 127 乘 255,还原到 [0, 255]

  • 暗影:直接除以 127 乘 255,还原到 [0, 255]

用 np.clip 防止溢出。

六、完整代码

编码代码:

复制代码
# stego_checkerboard.py
import numpy as np
from PIL import Image, ImageOps, ImageFilter


def generate_shadow(img_ori: Image.Image, mode: str = "invert") -> Image.Image:
    """根据原图生成暗影图。"""
    img_ori = img_ori.convert("RGB")

    if mode == "invert":
        # 反色阴影:解码后得到原图 + 反色图
        return ImageOps.invert(img_ori)
    elif mode == "gray":
        # 灰度阴影:解码后得到原图 + 灰度图
        return img_ori.convert("L").convert("RGB")
    elif mode == "edges":
        # 边缘阴影:解码后得到原图 + 边缘图
        return img_ori.filter(ImageFilter.FIND_EDGES).convert("RGB")
    elif mode == "blur":
        # 模糊阴影:解码后得到原图 + 模糊图
        return img_ori.filter(ImageFilter.GaussianBlur(radius=2))
    else:
        raise ValueError(f"不支持的 shadow_mode: {mode}")


def encode(origin_path,output_path,shadow_path,shadow_mode,):
    """
    将原图和暗影图合成为棋盘格隐写图。

    参数:
        origin_path : 原图路径
        output_path : 输出隐写图路径
        shadow_path : 暗影图路径。如果为 None,则根据原图自动生成
        shadow_mode : 自动生成暗影图的方式(invert / gray / edges / blur)
    """
    img_ori = Image.open(origin_path).convert("RGB")
    W, H = img_ori.size

    if shadow_path is not None:
        img_sha = Image.open(shadow_path).convert("RGB")
        img_sha = img_sha.resize((W, H), Image.LANCZOS)
    else:
        img_sha = generate_shadow(img_ori, shadow_mode)
        img_sha = img_sha.resize((W, H), Image.LANCZOS)

    # 转为 numpy 数组
    arr_ori = np.array(img_ori).astype(np.float32)
    arr_sha = np.array(img_sha).astype(np.float32)

    # 色彩空间映射:暗影 -> [0, 127],原图 -> [128, 255]
    arr_sha_adj = (arr_sha / 255.0) * 127.0
    arr_ori_adj = (arr_ori / 255.0) * 127.0 + 128.0

    # 创建画布 (2H, 2W, 3)
    canvas = np.zeros((2 * H, 2 * W, 3), dtype=np.uint8)

    # 棋盘格填充
    # 原图(奇相位)
    canvas[1::2, 0::2] = np.round(arr_ori_adj).astype(np.uint8)
    canvas[0::2, 1::2] = np.round(arr_ori_adj).astype(np.uint8)

    # 暗影(偶相位)
    canvas[0::2, 0::2] = np.round(arr_sha_adj).astype(np.uint8)
    canvas[1::2, 1::2] = np.round(arr_sha_adj).astype(np.uint8)

    Image.fromarray(canvas).save(output_path)
    print(f"隐写图已生成: {output_path}")
    print(f"尺寸: {2 * W}x{2 * H}")


def decode(stego_path: str, origin_out_path: str, shadow_out_path: str):
    """
    从棋盘格隐写图中解码出原图和暗影图。
    """
    stego = np.array(Image.open(stego_path).convert("RGB")).astype(np.float32)

    # 取出编码值
    arr_ori_adj = stego[1::2, 0::2]  # 原图
    arr_sha_adj = stego[0::2, 0::2]  # 暗影

    # 逆映射回 [0, 255]
    arr_ori = np.clip((arr_ori_adj - 128.0) / 127.0 * 255.0, 0, 255).astype(np.uint8)
    arr_sha = np.clip(arr_sha_adj / 127.0 * 255.0, 0, 255).astype(np.uint8)

    Image.fromarray(arr_ori).save(origin_out_path)
    Image.fromarray(arr_sha).save(shadow_out_path)
    print(f"解码原图已保存: {origin_out_path}")
    print(f"解码暗影图已保存: {shadow_out_path}")


if __name__ == "__main__":
    ORIGIN_IMG = "origin.png"
    STEGO_IMG = "stego_checkerboard.png"
    DECODED_ORIGIN = "decoded_origin.png"
    DECODED_SHADOW = "decoded_shadow.png"

    # 编码:暗影图由原图自动生成(反色)
    encode(ORIGIN_IMG, STEGO_IMG, shadow_path=None, shadow_mode="invert")

    # 解码
    decode(STEGO_IMG, DECODED_ORIGIN, DECODED_SHADOW)

得到隐写文件decoded_shadow.png。

解码代码:

复制代码
# checkerboard_stego.py --- 还原部分
import numpy as np
from PIL import Image


def _even_mask(h, w):
    """生成偶数相位的掩码矩阵"""
    yy, xx = np.mgrid[0:h, 0:w]
    return (xx + yy) % 2 == 0


def detect(src):
    """识别:判断一张图是否为棋盘格奇偶隐写"""
    a = np.array(Image.open(src).convert("RGB")).astype(np.int16)
    h, w, _ = a.shape
    even = _even_mask(h, w)

    e, o = a[even], a[~even]

    # 打印像素范围及均值信息
    print(f"偶相位值域 {e.min()} ~ {e.max()}   均值 {e.mean():.1f}")
    print(f"奇相位值域 {o.min()} ~ {o.max()}   均值 {o.mean():.1f}")

    if e.max() < o.min():
        print(">>> 两相位值域完全不重叠 --- 判定为棋盘格奇偶隐写")
    else:
        print(">>> 两相位值域有重叠 --- 需要进一步排查(可能是混合图或普通图)")


def decode(src, prefix):
    """还原:奇相位就是原图,偶相位是暗影版"""
    a = np.array(Image.open(src).convert("RGB"))
    h, w, _ = a.shape

    # 半尺寸提取

    odd_adj = a[1::2, 0::2]  # (x+y) 为奇数 的像素 -> 原图
    even_adj = a[0::2, 0::2]  # (x+y) 为偶数 的像素 -> 暗影
    odd = np.clip((odd_adj - 128.0) / 127.0 * 255.0, 0, 255).astype(np.uint8)
    even = np.clip(even_adj / 127.0 * 255.0, 0, 255).astype(np.uint8)

    # 不放大,直接保存半尺寸,避免 LANCZOS 引入模糊
    Image.fromarray(odd).save(prefix + "_原图.png")
    Image.fromarray(even).save(prefix + "_暗影.png")

    print(f"已输出 {prefix}_原图.png / {prefix}_暗影.png")


if __name__ == "__main__":
    SRC = r"stego_checkerboard.png"

    print("--- 开始检测 ---")
    detect(SRC)

    print("--- 开始还原 ---")
    decode(SRC, r"还原结果")
相关推荐
Xudde.1 小时前
Billu: box 靶机渗透测试(Vulnhub Writeup)
笔记·学习·安全·web安全
9624561 小时前
餐饮 SaaS 优惠券系统架构演进(九)营销策略 DSL 与规则生命周期:从运营配置、静态校验到编译执行、影响面分析与安全发布
安全·系统架构
薛定猫AI1 小时前
【技术干货】基于本地优先架构的开源AI智能体Octopus系统设计与实战解析
python·机器学习·ai·大模型·技术实战
迪康Defender1 小时前
终端安全行为分析:审计日志如何转化为业务报表?落地实践与避坑指南
运维·经验分享·安全·信息可视化·自动化·安全威胁分析·运维开发
技术不好的崎鸣同学1 小时前
第七章:模块踩踏以覆写导出函数绕过CFG
c++·windows·安全·系统安全
黄一一9112431 小时前
吾爱大佬开发,离线可用!PDF文件处理工具,本地网页版文件-浏览器里就能搞定,所有操作系统可用
windows·安全·阿里云·pdf·.net·开源软件
程序猿乐锅1 小时前
【从0-1学习Python | 第四篇】Python 字符串和元组
java·开发语言·笔记·后端·python·学习
Rebecca_aLi1 小时前
码匠教育:从手工表格到脚本自动化,Python 改造工作流的思路
运维·python·自动化
开开心心就好1 小时前
A3的PDF怎么切成A4?拖一下分割线就行
服务器·开发语言·python·算法·pdf·ocr·tornado