Python 实现图片隐写术(LSB + 四节点并行)
本文配套实验:Pillow 模块 / 最低有效位(LSB) / lambda 表达式递归 / UTF-8 编码,并在华为云 FlexusX 4 节点上完成分布式并行隐写实战。
完整源码与运行产物见文末仓库地址(Gitee)。
一、什么是图片隐写术
隐写术(Steganography)是把一段秘密信息“藏”进正常载体(图片、音频、视频)中,使第三方从外观上无法察觉信息存在的技术——它与加密不同:加密是“让内容看不懂”,隐写是“让内容看起来不存在”。
本文用最经典的LSB(Least Significant Bit,最低有效位)算法:把秘密信息的每一个比特,写进图片像素 RGB 通道的最低那一位。由于最低位对颜色影响极小(例如(123,45,67)改成(122,45,67)肉眼根本分辨不出),载体图片看起来“毫无变化”,但信息已经藏进去了。
二、本次实验的知识点
| 知识点 | 在实验中的作用 |
|---|---|
| 1. Pillow 模块 | 生成/读写图片,做像素级getdata()/putdata()操作 |
| 2. 最低有效位 (LSB) | 把秘密比特藏入像素 RGB 通道最低位:v & 0xFE | bit |
| 3. lambda 表达式递归 | 用“自应用递归 lambda(Y 组合子风格)”完成bytes ↔ bits与逐比特嵌入/提取 |
| 4. UTF-8 编码 | 中文/英文/emoji 先encode("utf-8")成字节流,再按比特嵌入 |
三、LSB 隐写原理(图文)
一张W×H的 RGB 图片,共有W×H×3个 0~255 的颜色分量。每个分量用 8 bit 表示,最低位(第 0 位)对颜色贡献只有1,改掉它肉眼几乎无感。
嵌入一个比特b到颜色分量v:
v' = (v & 0xFE) | bv & 0xFE:把最低位清零(保留高 7 位)| b:写入秘密比特b(0 或 1)
提取时只需v' & 1即可拿回该比特。
容量估算:本实验用
200×200封面,容量 =200×200×3 = 120000 bit ≈ 14.6 KB文本,足以藏下一篇短文。
为了知道“从哪一位开始、读到哪一位结束”,我们在数据前加一个32 位消息长度头:先写消息比特总数,提取端先读 32 位得到长度,再精确读后续比特。
四、UTF-8:多字节字符的正确打开方式
秘密文本往往是中文或 emoji,例如华为云FlexusX四节点并行隐写术✅2026DataHelper。这些字符在内存里不是按“字”而是按字节存储的:
- ASCII 字符(如
F、2、D)→ 1 字节 - 常用汉字(如
华、云)→ 3 字节(UTF-8) - emoji(如
✅)→ 4 字节
所以绝不能“一个字当一个字节”处理,否则中文会乱码。正确做法:
data=fragment.encode("utf-8")# str -> bytes(UTF-8 多字节)# ...嵌入比特...recovered=to_bytes(msg_bits,0).decode("utf-8")# bytes -> str各节点真实编码数据(来自本次运行summary.json):
| 节点 | 片段 | 字符数 | UTF-8 字节数 | 消息比特 |
|---|---|---|---|---|
| 0001 | 华为云Flexus | 9 | 15 | 120 |
| 0004 | X四节点并行隐写 | 8 | 22 | 176 |
| 0002 | 术✅2026Da | 8 | 12 | 96 |
| 0003 | taHelper | 8 | 8 | 64 |
可以看到:同样 8 个字符,X四节点并行隐写是 22 字节(含 6 个汉字 ×3 + 2 个 ASCII),而taHelper只有 8 字节(全 ASCII)——这正是 UTF-8 变长编码的体现。
五、lambda 表达式递归:不用def也能递归
很多人以为 Python 的lambda不能递归(因为没有名字)。其实用自应用(Y 组合子风格)即可:把函数自身f作为参数传给自己,再在自己内部调用f(f)。
1) bytes → bits(逐字节拆 8 比特)
to_bits=(lambdaf:f(f))(lambdaf:lambdabs,i:[]ifi>=len(bs)else[(bs[i]>>k)&1forkin(7,6,5,4,3,2,1,0)]+f(f)(bs,i+1))msg_bits=to_bits(list(data),0)# 字节流 -> 0/1 比特列表- 终止条件:
i >= len(bs)返回空列表 - 递归步:取第
i字节,按高位到低位7→0取出 8 个比特,拼到后续结果前
2) LSB 嵌入(逐比特改写最低位,原地修改)
embed=(lambdaf:f(f))(lambdaf:lambdaarr,bits,p,b:(arr.__setitem__(p,(arr[p]&0xFE)|bits[b]),f(f)(arr,bits,p+1,b+1))[-1]ifb<len(bits)elseNone)embed(flat,all_bits,0,0)# flat: 一维像素分量列表- 用
arr.__setitem__(p, (arr[p] & 0xFE) | bits[b])原地改最低位 - 元组
(set, recurse)[-1]的写法只是为了把“副作用”塞进 lambda 的单一表达式里
3) 提取 + bits → bytes
extract=(lambdaf:f(f))(lambdaf:lambdaarr,cnt,p:[]ifp>=cntelse[arr[p]&1]+f(f