
简介围绕BMP、JPG、GIF三种常见图片格式的隐写实现这份C源码包面向信息安全学习者、课程设计开发者以及需要理解图片信息隐藏原理的读者帮助其从代码层面认识文件头篡改、像素数据利用与尾部追加等思路。包内共5个文件以4个cpp源文件和1个h头文件为主分别承载BMP尺寸处理、主流程组织以及JPG、GIF隐写实现压缩包约5KB体量轻便便于快速浏览与本地编译调试。内容涉及BMP文件头与像素区嵌入、JPEG压缩域与DCT系数调整、GIF的LZW压缩特性及扩展块/全局颜色表利用并通过头文件组织接口便于对照三种格式的差异。已有6127人学习适合作为信息安全实验、隐写分析入门和版权保护相关课题的参考代码也可在此基础上做检测与提取模块的二次开发对信息隐藏的安全性与不可察觉性取舍形成直观认识。1. BMP、JPG、GIF 图片隐写先分清三条路线为什么有人能读回有人一保存就翻车我常遇到一个场景一段校验串明明已经写进图片发给对方再保存一次读出来却是乱码。BMP、JPG、GIF 图片隐写看着都是“往图里塞数据”实际是三条完全不同的工程路线。BMP 无压缩改像素字节的最低位最直接JPG 要经过 DCT 量化和熵编码像素 LSB 很容易被有损链路洗掉得盯系数或文件段GIF 是索引色调色板顺序、注释扩展和帧结构比硬改 RGB 更稳。下面按容量、参数、代码和排查顺序拆开讲适合手里有合法素材、要做水印、校验串或演示程序的开发者。2. BMP 隐写从文件头偏移到像素低位先把容量算准2.1 BMP 的文件结构为什么适合做 LSB头、调色板与像素阵列BMP 是理解图片隐写最好的起点因为它把“文件头”和“像素数据”分得很清楚。常见的 24 位 BMP 由三部分组成14 字节的文件头、40 字节的信息头、以及从bfOffBits开始的像素阵列。文件头偏移 10 的 4 字节是bfOffBits它直接告诉你像素从哪里开始信息头偏移 28 的 2 字节是biBitCount常见值是 24 或 32偏移 30 的 4 字节是压缩方式BI_RGB为 0表示不压缩。做 LSB 隐写时最怕把数据写进文件头或调色板导致图片打不开所以第一步永远是读bfOffBits不是凭感觉从第 54 字节开始写。24 位 BMP 的像素按 B、G、R 顺序存放每行还要按 4 字节对齐行尾可能有 1 到 3 个填充字节。填充字节不参与显示但也在像素区里。有人喜欢把负载写进填充区容量小且不同解码器处理不一致我一般不用除非只是做实验。32 位 BMP 多一个 Alpha 通道改 Alpha 的最低位可能让透明区域出现细微变化所以更稳的做法是只改蓝色通道或者跳过每 4 字节的第 4 字节。8 位 BMP 带调色板改索引会牵动颜色表复杂度和 GIF 类似入门阶段先不碰。容量公式要先落到“像素区字节数”上pixel_bytes file_size - bfOffBits。如果每字节只用 1 个最低位理论负载容量是pixel_bytes // 8再扣掉自己定义的协议头。协议头不能省至少要有魔数和长度否则解码时不知道负载在哪里结束。常见做法是写 4 字节魔数加 4 字节小端长度再写 payload。魔数用来排除“这张图根本没写过”的情况长度用来防止把后面的像素噪声当成数据。BMP 没有压缩二进制复制就能保留这是它比 JPG 省心的地方代价是文件大网络传输时容易被二次压缩。参数位置/含义常见取值隐写时的作用bfOffBits文件头偏移 104 字节54、138 等像素起点写入必须从这里开始biBitCount信息头偏移 282 字节24、32决定每像素字节数和通道biCompression信息头偏移 304 字节0BI_RGB非 0 时不要按裸像素写行填充每行按 4 字节对齐0~3 字节可读但不要依赖它藏数据2.2 用 Python 标准库写入和读回 LSB最小可复现脚本下面这段代码只依赖标准库处理 24/32 位、BI_RGB的 BMP。它先写 8 字节协议头再把每个字节的最低位替换成 payload 的比特。解码时按同样的顺序读回先校验魔数再读长度。代码里没有图像库目的是让文件结构透明方便你排查每一步。import struct MAGIC bLSB1 def _bmp_pixel_offset(data): if data[:2] ! bBM: raise ValueError(不是BMP文件) off struct.unpack_from(I, data, 10)[0] bpp struct.unpack_from(H, data, 28)[0] comp struct.unpack_from(I, data, 30)[0] if bpp not in (24, 32): raise ValueError(只处理24/32位BMP) if comp ! 0: raise ValueError(只处理BI_RGB未压缩BMP) if off len(data): raise ValueError(bfOffBits越界) return off def bmp_lsb_encode(src, dst, payload: bytes): data bytearray(open(src, rb).read()) off _bmp_pixel_offset(data) blob MAGIC struct.pack(I, len(payload)) payload need_bits len(blob) * 8 if need_bits len(data) - off: raise ValueError(容量不足) for i in range(need_bits): bit (blob[i // 8] (7 - (i % 8))) 1 pos off i data[pos] (data[pos] 0xFE) | bit open(dst, wb).write(data) def bmp_lsb_decode(path): data open(path, rb).read() off _bmp_pixel_offset(data) raw bytearray() total_bytes (len(data) - off) // 8 for n in range(total_bytes): b 0 for k in range(8): b (b 1) | (data[off n * 8 k] 1) raw.append(b) if len(raw) 4 and raw[:4] ! MAGIC: raise ValueError(魔数不匹配可能不是LSB1载体) if len(raw) 8: length struct.unpack(I, raw[4:8])[0] if len(raw) 8 length: return bytes(raw[8:8 length]) raise ValueError(未读到完整负载) # 用法payload 是任意 bytes长度先自己算好 bmp_lsb_encode(cover.bmp, stego.bmp, border2024;check3f9a;) print(bmp_lsb_decode(stego.bmp))逻辑说明_bmp_pixel_offset做三件事校验BM、读bfOffBits、检查位深和压缩方式。bmp_lsb_encode把魔数、长度、payload 拼成blob按高位到低位逐 bit 写进像素区每个字节的最低位。bmp_lsb_decode反过来每 8 个字节拼一个字节先看魔数再看长度长度够了就返回。参数上payload建议控制在容量的一半以内留出余量dst必须用二进制写入不能经过图像编辑器再保存。24 位图每个像素 3 字节代码会顺带改到 G、R 通道的最低位视觉上通常看不出来但如果原图有大面积纯色改多了可能出现轻微噪点。32 位图会改到 Alpha 的最低位透明图要谨慎。提示写入前先用len(blob)*8和len(data)-off做一次硬校验不要等写完才发现越界。BMP 的行填充不影响这段代码因为我们是按像素区字节顺序连续写的不按行坐标写。2.3 容量、鲁棒性与不可见性的取舍bit 数、通道和填充区BMP 的容量账很好算但“能写多少”和“写多少还稳”是两回事。以 1920×1080 的 24 位 BMP 为例像素区约1920*1080*36220800字节1 bit/字节的纯理论容量约 777600 字节扣掉协议头还有 777592 字节。如果你只改蓝色通道每像素只有 1 个字节可用容量降到约 259200 字节。改成每字节 2 个最低位容量翻倍但颜色偏移和噪声会明显增加尤其在渐变天空、皮肤、纯色背景上容易被肉眼或直方图看出。我的习惯是默认 1 bit、优先蓝通道、payload 不超过容量的 30%先保证读得回再谈藏得多。载体写入方式理论容量1920×1080风险24 位 BMP每字节 1 bit约 777600 字节三通道都轻微变化24 位 BMP只改蓝通道 1 bit约 259200 字节容量小颜色更稳24 位 BMP每字节 2 bit约 1555200 字节噪声和色偏上升32 位 BMP跳过 Alpha改 BGR约 583200 字节透明区域相对安全鲁棒性方面BMP 的 LSB 只抗“二进制复制”不抗缩放、裁剪、转 JPG、聊天软件压缩。有人把 BMP 发到即时通讯工具对方收到后变成 JPGLSB 会成片丢失这是最常见的翻车点。行填充区可以写但不同软件重存 BMP 时可能重算填充字节所以不要把关键协议头放在填充区。填充区更适合做“干扰位”或实验标记。另一个细节是调色板8 位 BMP 的调色板在bfOffBits之前改调色板会影响显示24/32 位没有调色板写入边界最清晰。选型上BMP 适合做本地校验、离线演示、教学基线不适合需要跨平台传输后仍能读回的场景。把 BMP 当成“可控环境下的低成本载体”心态会稳很多。3. JPG 隐写DCT 系数和 COM 段两条路别在像素上硬改3.1 JPG 的有损链路为什么像素 LSB 在 JPG 上容易翻车JPG 的编码链路比 BMP 长得多颜色转换、8×8 分块、DCT 变换、量化、Z 字扫描、熵编码。你看到的像素是解码后的结果真正决定文件的是量化后的 DCT 系数和熵编码比特流。像素 LSB 在 JPG 上不成立因为任何一次重新保存都会重新做 DCT 和量化最低位被量化步长直接抹掉。我见过最典型的失败是用脚本改了解码后的 RGB 最低位再存成 JPG读回时前几个字节还对后面全乱。不是脚本写错了是链路本身不允许。JPG 隐写常见有三条路。第一条是改量化后的 DCT 系数通常选中频 AC 系数把系数的最低位换成 payload bit再重写熵编码。这条路容量小鲁棒性中等画质影响可控但实现复杂需要解析量化表、Huffman 表、重启间隔还要处理系数溢出。第二条是微量调整量化表把信息编码在量化步长的奇偶或排序里容量更小重编码后基本消失。第三条是文件段比如 COM 注释段或 APP 段直接塞字节不碰像素写入和读取都简单但任何转码、重存、平台压缩都会丢。我的建议是做短校验串、配置标记优先 COM 段做研究或需要抗一定重编码再碰 DCT 系数。别把“像素 LSB 改 JPG”当成可行路线那是拿有损链路当无损通道。参数上JPG 的质量因子会改变量化表。质量越低量化步长越大DCT 系数被改后越容易被量化抹掉质量越高容量和稳定性好一些但文件更大。中频位置一般选(2,1)、(1,2)、(2,2)这类避开 DC(0,0)和低频(0,1)、(1,0)因为低频改一点就会出块效应。每个 8×8 块最多放 1 到 2 bit 比较稳放多了画质和误码率都会上来。3.2 不改像素改系数用 DCT 块中频 AC 系数藏 1 bit 的最小流程完整 JPG 比特流写入需要熵编码器不适合用几十行代码硬凑。下面这段是纯 Python 的 DCT 域教学版构造 8×8 块做 DCT量化把(2,1)位置量化系数的奇偶改成指定 bit再逆变换看误差。它能帮你理解“系数奇偶”与“画质”的关系但不是完整 JPG 文件写入器。真实落地时你需要把这一步接到解析量化表、修改系数、重新熵编码的流程里。import math def dct8(block): N 8 out [[0.0] * N for _ in range(N)] for u in range(N): for v in range(N): s 0.0 for x in range(N): for y in range(N): s block[x][y] * math.cos((2*x1)*u*math.pi/(2*N)) * math.cos((2*y1)*v*math.pi/(2*N)) cu 1 / math.sqrt(2) if u 0 else 1.0 cv 1 / math.sqrt(2) if v 0 else 1.0 out[u][v] 0.25 * cu * cv * s return out def idct8(coef): N 8 out [[0.0] * N for _ in range(N)] for x in range(N): for y in range(N): s 0.0 for u in range(N): for v in range(N): cu 1 / math.sqrt(2) if u 0 else 1.0 cv 1 / math.sqrt(2) if v 0 else 1.0 s cu * cv * coef[u][v] * math.cos((2*x1)*u*math.pi/(2*N)) * math.cos((2*y1)*v*math.pi/(2*N)) out[x][y] 0.25 * s return out def set_parity(coef, pos, bit, q10.0): u, v pos qc round(coef[u][v] / q) if qc % 2 ! bit: qc 1 coef[u][v] qc * q return coef # 构造一个 8x8 灰度块模拟量化后的系数奇偶修改 block [[float((x*17 y*31) % 256) for y in range(8)] for x in range(8)] coef dct8(block) q 10.0 quant [[round(coef[u][v] / q) for v in range(8)] for u in range(8)] set_parity(quant, (2, 1), 1, q) recon idct8(quant) mse sum((block[x][y] - recon[x][y]) ** p a hrefhttps://download.csdn.net/download/qq_36554318/10435196 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p