隐形水印是将数据嵌入文件冗余位(如bmp的lsb、wav的pcm末位)而不改变外观或听感的技术;c#需手动操作字节,用lockbits或直接读写wav样本,禁用graphics等可视化方法。

什么是隐形水印,它在 C# 里根本不是“加一层透明图”
隐形水印(Steganography)和可视化水印完全不是一回事。它不改变文件外观或听感,而是把数据藏进文件的冗余位里——比如 Bitmap 的最低有效位(LSB),或 WAV 的 PCM 样本末位。C# 没有内置 API 做这个,得自己操作原始字节或像素。别用 Graphics 或 DrawString,那只是明水印。
- 图像常用载体:24 位
BMP或未压缩的Bitmap(PNG行为不可控,JPEG会破坏 LSB) - 音频常用载体:
WAV(PCM 编码、16-bit、单/双声道),避免 MP3/AAC —— 它们是破坏性压缩,藏进去秒丢 - 核心操作对象是
byte[]或int[](像素值),不是Image对象本身
用 Bitmap.LockBits 写入 LSB 水印(图像场景)
LockBits 是唯一靠谱的路径:它绕过 GDI+ 封装,直接拿到内存中像素的原始字节指针。用 GetPixel/SetPixel 不仅慢,还会触发颜色空间转换,导致 LSB 错乱。
- 只支持
PixelFormat.Format24bppRgb或Format32bppArgb;其他格式(如 8bpp)需先转换 - 水印文本必须转成字节流(
Encoding.UTF8.GetBytes()),再逐 bit 填进每个像素的R、G、B通道最低位 - 务必在写入前检查图像容量:总像素 × 3(RGB) ≥ 水印字节数 × 8(bit);否则截断或报错
- 示例关键片段:
var bits = bitmap.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb); unsafe { byte* ptr = (byte*)bits.Scan0.ToPointer(); for (int i = 0; i < data.Length; i++) { for (int b = 0; b < 8; b++) { int pixelOffset = (i * 8 + b) * 3; ptr[pixelOffset + 0] = (byte)((ptr[pixelOffset + 0] & 0xFE) | ((data[i] >> (7 - b)) & 0x01)); } } } bitmap.UnlockBits(bits);
从 WAV 文件读取 PCM 数据并嵌入(音频场景)
WAV 头部固定 44 字节,之后是纯 PCM 样本。16-bit 单声道下,每样本占 2 字节;LSB 隐藏就改这 2 字节的最低位。别碰头部,也别用 SoundPlayer 或 NAudio 的高级封装——它们不暴露原始样本缓冲区。
- 用
File.ReadAllBytes()读整个文件,跳过前 44 字节,剩余部分就是样本数据 - 样本是小端序
Int16,用BitConverter.ToInt16()解包;改完最低位后用BitConverter.GetBytes()写回 - 双声道要交错处理:左声道样本在偶数索引,右声道在奇数索引;水印 bit 流需均匀分给左右
- 错误现象:
System.ArgumentException: Parameter is not valid—— 很可能是 WAV 头部解析错,或样本长度没对齐 2 字节
提取水印时最常踩的三个坑
藏进去容易,取出来失败率极高。90% 的问题出在“以为藏哪就能从哪原样读回来”。
- 图像:保存为
JPEG后再读取 → 所有 LSB 全乱,因为 DCT 量化会抹掉最低位;必须保持BMP或用Save(..., ImageFormat.Bmp) - 音频:用不同采样率/位深重采样过的
WAV→ 样本值已变,LSB 不再对应原始嵌入位置 - 通用:没嵌入结束标记(如 0xFF 0xFF)→ 提取时不知道水印在哪结束,容易读出乱码;建议在数据前加 4 字节长度头
LSB 隐写本质脆弱,它不抗编辑、不抗转码、不抗剪切。真要防扩散,得结合加密(先 Aes.Encrypt 再嵌入)和校验(加 CRC32),但那是另一层事了。