news 2026/10/26 20:56:57

BMP、JPG、GIF图片隐写:LSB、DCT系数与文件段路线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
BMP、JPG、GIF图片隐写:LSB、DCT系数与文件段路线

简介:围绕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文件头偏移 10,4 字节54、138 等像素起点,写入必须从这里开始
biBitCount信息头偏移 28,2 字节24、32决定每像素字节数和通道
biCompression信息头偏移 30,4 字节0(BI_RGB)非 0 时不要按裸像素写
行填充每行按 4 字节对齐0~3 字节可读但不要依赖它藏数据

2.2 用 Python 标准库写入和读回 LSB:最小可复现脚本

下面这段代码只依赖标准库,处理 24/32 位、BI_RGB的 BMP。它先写 8 字节协议头,再把每个字节的最低位替换成 payload 的比特。解码时按同样的顺序读回,先校验魔数,再读长度。代码里没有图像库,目的是让文件结构透明,方便你排查每一步。

import struct MAGIC = b'LSB1' def _bmp_pixel_offset(data): if data[:2] != b'BM': 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', b'order=2024;check=3f9a;') 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*3=6220800字节,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 发到即时通讯工具,对方收到后变成 JPG,LSB 会成片丢失,这是最常见的翻车点。行填充区可以写,但不同软件重存 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*x+1)*u*math.pi/(2*N)) * math.cos((2*y+1)*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*x+1)*u*math.pi/(2*N)) * math.cos((2*y+1)*v*math.pi/(2*N)) out[x][y] = 0.25 * s return out def set_parity(coef, pos, bit, q=10.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 href="https://download.csdn.net/download/qq_36554318/10435196" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 20:52:58

labelme转YOLO关键点格式:坐标归一化与可见性编码实战

做关键点检测项目的人多半都经历过这个瞬间&#xff1a;在 labelme 里辛辛苦苦点完几十张图的关键点&#xff0c;导出 JSON 一看&#xff0c;信心满满地丢给 YOLO 训练脚本&#xff0c;结果第一行就报错——找不到标签文件&#xff0c;或者找到了但格式对不上。我最初做自用关键…

作者头像 李华
网站建设 2026/10/26 20:52:55

水下图像增强与桥墩裂缝检测技术解析

1. 项目概述&#xff1a;水下图像增强与桥墩检测的痛点结合水下结构物检测一直是工程维护中的难点领域&#xff0c;特别是桥墩这类常年浸泡在水中的关键承重部件。我参与过多次跨海大桥的定期检测&#xff0c;最头疼的就是浑浊水体中拍摄的裂缝图像——那些本该清晰呈现的纹理细…

作者头像 李华
网站建设 2026/10/26 20:52:46

后端开发者转型AI:思维重构与工程实践

1. 从后端到AI&#xff1a;技术转型的底层逻辑2019年至今&#xff0c;AI工程化领域出现了明显的技术断层。传统后端开发者在面对LLM、Agent、RAG等技术栈时&#xff0c;往往陷入"看得懂论文但落不了地"的困境。我亲身经历了从Java/Python后端到AI技术专家的转型过程&…

作者头像 李华
网站建设 2026/10/26 20:52:46

后端开发者转型AI:技术栈迁移与Agent系统实战

1. 从后端到AI的转型之路&#xff1a;技术栈迁移与能力重塑 两年前&#xff0c;当我决定从后端开发转向AI领域时&#xff0c;最深刻的体会是&#xff1a;技术栈的差异远没有想象中那么大。后端开发积累的系统思维和工程能力&#xff0c;恰恰是构建高质量AI系统的关键基础。我的…

作者头像 李华
网站建设 2026/10/26 20:52:39

AI模块化能力封装:Skills概念与OpenClaw架构解析

1. Skills概念深度解析1.1 什么是SkillsSkills本质上是一种模块化的能力封装单元&#xff0c;类似于我们日常使用的手机APP。想象一下&#xff0c;你的智能手机本身只是一个硬件平台&#xff0c;真正赋予它实用价值的是各种功能明确的应用程序。Skills在AI领域扮演着相似的角色…

作者头像 李华
网站建设 2026/10/26 20:49:21

Stata文本分析新范式:catllm实现文本分类与主题发现一键结构化

1. 项目概述&#xff1a;当 Stata 遇上大模型&#xff0c;不是“调用 API”而是“打通工作流”Stata 用户看到“调用大模型”这五个字&#xff0c;第一反应往往是皱眉——这玩意儿和我写reg y x1 x2 i.year, vce(cluster id)有啥关系&#xff1f;是不是又要装 Python、配环境、…

作者头像 李华