
封面:模拟直播画面叠加贴片水印(本项目 E2E 真实产物)
01 为什么需要一个图像水印基础库
直播和短视频是搬运重灾区:同一场直播被 OBS 多开转推、录屏改标题再发,平台去重和盗播溯源全靠人工;电商主图被同行一键盗用、内部截图外泄找不到人,同样是普遍痛点。这些场景有一个共同需求——给图片或画面嵌入身份标识,事后能提取验证,出事了能溯源到人。
在 MoonBit 生态里,这个能力是空白的:mooncakes 上现成的图像包有合成滤镜、有 LLM 文本水印、有流处理事件水印,唯独没有「图像水印嵌入 → 提取 → 溯源」的完整链路。moon-watermark 就以通用基础库的定位补上这一块:可见水印做威慑,不可见水印做取证,溯源 API 定位泄漏源。
02 不可见水印的两条路线
不可见水印要解决的核心问题是:在用户看不出的前提下,把信息藏进图片里,而且尽量扛得住图片分发链路的各种折腾。
最简单的路线是 LSB——把 payload 的每个 bit 写进像素 RGB 通道的最低位。实现直观,PNG 无损往返后提取几乎不损失,实测 PSNR 76.98dB,肉眼完全不可察。但它有个致命弱点:JPEG 量化会把最低位的信息几乎全部抹掉,一旦图片过压缩链路,LSB 就废了。
更稳的路线是 DCT 域:把图片按 8×8 分块做 DCT 变换,把 bit 写进中频系数的奇偶性(QIM 量化)。JPEG 本身就是 DCT 域量化编码,因此 DCT 域水印天然更抗压缩。我们的库两条路线都做了,按分发链路选:PNG 无损链路走 LSB,JPEG/压缩链路走 DCT。
03 翻车现场:纯色测试图全过,真实照片翻车
最初版本的 DCT 水印是在 G 通道上做的。写完后跑测试:纯色图、渐变图、简单纹理图,嵌入 → PNG 往返 → 提取,全部通过,PSNR 也漂亮。
直到把测试图换成照片风格的纹理图,再走一遍真实链路——嵌入 → JPEG q85 转码 → 提取。结果提取失败。单测全绿,真实链路翻车,这是最典型的「测试没贴近真实场景」事故。
排查根因:JPEG 编码不是在 RGB 空间直接量化的,而是先转到 YCbCr,把亮度 Y 和两个色度分量分开,再对色度通道做更狠的降采样和量化。水印藏在 G 通道里,而 G 通道的信息在 YCbCr 转换和色度量化往返中被打散了,系数奇偶性不再稳定,提取自然失败。
为什么纯色测试图骗了我们?纯色图里 G 分量和亮度分量几乎重合,色度量化损失极小,往返后 G 通道系数还能稳住;照片的色度变化大,量化误差一上来,问题就暴露了。
04 修复:亮度域,和 JPEG 站在同一边
修复方案很直接:不在 G 通道嵌,改在亮度域嵌。用 BT.601 公式 Y = 0.299R + 0.587G + 0.114B 把像素转成亮度,在亮度平面的 DCT 系数上做 QIM 嵌入。
这样做的关键收益是:我们嵌入的域,和 JPEG 编码保留的域是同一个。JPEG 对亮度通道的量化远比色度温和,亮度域系数经过编码往返后依然稳定,水印也就活下来了。
实测结果:照片风格纹理图,JPEG q85 转码后提取通过(默认 delta=24);更强压缩 q60 也通过(delta=48)。PSNR 分别 50.9dB 和 45.3dB,都在肉眼不可察的 40dB 线以上。
这次修复留了两个教训:第一,测试图必须贴近真实链路,纯色图会掩盖域不匹配的问题;第二,嵌入域要和载体编码域对齐——JPEG 的 YCbCr 不是你直觉里的 RGB,水印必须站在编码器保留的那一侧。
05 配套工程:中频系数、帧头与组合水印
除了换域,DCT 水印还有几个配套取舍。系数位置选在中频 (4,1):避开 DC 和低频主能量(那里改动一眼可见),也避开最高频(那里压 JPEG 先丢),中频在鲁棒性和隐蔽性之间最平衡。
量化步长 delta 是可调参数,越大越抗压缩、可见性略升,调用方按压缩强度自己权衡。提取侧靠帧头的 magic 标记和长度校验确认水印存在,这就是盲检测 detect_dct——拿到疑似泄漏图先判断「是不是被标记过」,再决定要不要深入提取。
库还提供组合入口 embed_all:一次调用叠加文本、点阵、LSB、DCT 多通道(LSB 与 DCT 互斥返回错误,不静默降级),面向直播贴片这类逐帧场景复用。配套的还有可见文本水印内置 Cjk16 中文点阵(GB2312 一级字 3755 个 + ASCII/符号 96 个,中英混合一条渲染)、确定性点阵溯源、配置 JSON 序列化、越界自动收缩等能力,以及 102 个测试和零警告 CI。
06 真实场景验证
为了验证「真实链路能跑」,我们做了端到端演示:生成一张模拟直播画面,叠加上文本水印(直播 UID)、点阵水印(观众 ID)、DCT 水印(UID),然后走 JPEG q85 转码模拟平台切片,最后提取验证。
结果:点阵命中率 1.0 通过;DCT 域提取出 UID-00421,定位到观众;trace_dots 在候选 seed 中锁定泄漏源。整条「嵌入 → 压缩 → 溯源」链路在真实场景里是通的。
鲁棒性矩阵也如实记录边界:缩放攻击下点阵水印仍可提取、LSB 抗裁剪但 DCT 不抗几何攻击;不可见水印提供隐蔽性与 JPEG 鲁棒性,不是强加密,密钥由持有方管理。把边界写清楚,比宣称全能更诚实。

图1:嵌入前后对比——LSB 水印在 512×288 图上仅改动 28 个像素,肉眼不可察(项目实测产物)
07 尾声:三分钟用起来
在 MoonBit 项目里执行 moon add yuzhiblue/moon-watermark,即可使用全部 embed / extract / verify / trace API;仓库提供文件模式 CLI(embed、verify-dots、verify-lsb、verify-dct 子命令)和一键可复现的性能基准、E2E 示例。
欢迎试用、提 issue,也欢迎把亮度域这套「嵌入域对齐编码域」的思路用在其他载体格式上——换一个格式,先想清楚它量化的是哪个域。