PHP 盲水印库:实现不可见图像水印
| 指标 | 旧版 | 新版 |
|---|---|---|
| 嵌入耗时 (940×940) | ~4.9 s | ~2.5 s(提速 2 倍) |
| 提取耗时 | ~3.8 s | ~1.5 s(提速 2.5 倍) |
| 嵌入+提取峰值内存 (940×940) | 必须设置 memory_limit=2G |
~61 MB(默认 128 MB 即可运行) |
| 嵌入+提取峰值内存 (1358×780) | 128 MB 限制下直接 OOM | ~81 MB |
| 图像画质 (PSNR, 940×940) | ~36.9 dB | 35.6 dB |
| 鲁棒性 (椒盐/亮度/遮挡/裁剪) | PASS | PASS |
| 鲁棒性 (缩放攻击, 普通照片) | PASS | PASS |
PSNR 35–40 dB 在一般照片上被公认为「视觉无损」。
默认d1/d2由 36/20 降低为 27/15,在保留对椒盐、亮度、遮挡、裁剪攻击的鲁棒性的前提下,进一步提升画质。
GD 扩展用于所有图像 I/O(读取 JPEG/PNG、写入 PNG,以及缩放/椒盐噪声/亮度等攻击模拟)。
可通过php -m | grep gd检查是否已启用。
composer require kid1296/blind-watermark
所有示例均可在 PHP 默认内存限制下直接运行,无需再加
-d memory_limit=2G
(即使是 130 万像素图像,峰值内存也远低于 128 MB)。
原始图片 + 水印文字 = 携带盲水印的不可见水印图片。
+ 'This is a test sentence.' → 
<?php
require __DIR__ . '/../vendor/autoload.php';
use Kid1296\BlindWatermark\WaterMark;
// ========== 嵌入 ==========
$bwm1 = new WaterMark(passwordWm: 1, passwordImg: 1);
$bwm1->read_img('examples/pics/origin.jpg');
$wm = 'This is a test sentence.';
$bwm1->read_wm($wm, 'str');
$bwm1->embed('examples/output/embedded_str.png');
$lenWm = $bwm1->wmSize; // 记得保存这个长度
// ========== 提取 ==========
$bwm2 = new WaterMark(passwordWm: 1, passwordImg: 1);
$extracted = $bwm2->extract(
filename: 'examples/output/embedded_str.png',
wmShape: $lenWm,
mode: 'str'
);
// $extracted === 'This is a test sentence.'
运行输出:
embed done in 2.51s
Put down the length of wm_bit 191
extract done in 1.49s
extracted: This is a test sentence.
RESULT: PASS
嵌入任意 0/1 数组,适合自定义协议或二进制载荷。
+ 1011010011010110 → 
<?php
require __DIR__ . '/../vendor/autoload.php';
use Kid1296\BlindWatermark\WaterMark;
$bits = [1,0,1,1,0,1,0,0,1,1,0,1,0,1,1,0];
// 嵌入
$bwm1 = new WaterMark(1, 1);
$bwm1->read_img('examples/pics/origin.jpg');
$bwm1->read_wm($bits, 'bit');
$bwm1->embed('examples/output/embedded_bit.png');
$lenWm = $bwm1->wmSize;
// 提取
$bwm2 = new WaterMark(1, 1);
$extracted = $bwm2->extract(
filename: 'examples/output/embedded_bit.png',
wmShape: $lenWm,
mode: 'bit' // 返回 int[],每个元素为 0 或 1
);
// $extracted === $bits
运行输出:
original bits: 1011010011010110
embed done in 2.64s, wm_size = 16
extract done in 1.55s
extracted bits: 1011010011010110
RESULT: PASS
适合 Web 服务场景:从请求/数据库读图→嵌入→直接返回 BGR 数据,全程不写临时文件。
完整示例:03_in_memory.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Kid1296\BlindWatermark\WaterMark;
use Kid1296\BlindWatermark\Image\ImageIo;
$img = ImageIo::read('examples/pics/origin.jpg');
// $img = ['data'=>string, 'h'=>int, 'w'=>int, 'channels'=>int]
// 嵌入 — 不传入 filename,直接返回内存结果数组
$bwm1 = new WaterMark(1, 1);
$bwm1->read_img($img); // 传数组而不是文件路径
$bwm1->read_wm('This is a test sentence.', 'str');
$embedded = $bwm1->embed(); // 不传 filename → 返回数组
$lenWm = $bwm1->wmSize;
// $embedded 结构:['data'=>string, 'h'=>int, 'w'=>int, 'channels'=>int]
// 可以稍后用 ImageIo::write(...) 落盘,或直接流式返回给客户端。
// 提取 — 传 embedImg 数组(而不是 filename)
$bwm2 = new WaterMark(1, 1);
$extracted = $bwm2->extract(embedImg: $embedded, wmShape: $lenWm, mode: 'str');
运行输出:
host image: 940x940, channels=3
embed done in 2.16s
embedded: 940x940, channels=3, data size = 21206400 bytes
extract done in 1.18s
extracted: This is a test sentence.
RESULT: PASS
演示水印在常见后处理攻击下的存活能力,以及裁剪 + 参数估计 + 还原的完整流程。
嵌入后的水印图(宿主 = origin.jpg,水印 = 'This is a attack test'):

各攻击结果图(以下每张图均能成功提取水印):
| 椒盐噪声 2% | 亮度 ×0.9 | 缩放 940²→800×600 |
|---|---|---|
![]() |
![]() |
![]() |
| 遮挡 5块@5% | 裁剪右侧 20% | 裁剪还原后 |
|---|---|---|
![]() |
![]() |
![]() |
完整示例:04_with_attacks.php
$ php examples/04_with_attacks.php
embedded wm_size = 167
-- attacks --
salt-pepper 2% -> 'This is a attack test' [PASS]
brightness 0.9 -> 'This is a attack test' [PASS]
resize 940x940 -> 800x600 -> back -> 'This is a attack test' [PASS]
shelter 5 blocks @5% -> 'This is a attack test' [PASS]
-- crop + recover --
crop right 20% + recover -> 'This is a attack test' [PASS]
estimate_crop_parameters -> [0,0,752,940] (与期望值一致) [PASS]
关于缩放攻击的鲁棒性说明:缩放攻击内部使用 GD 的双线性重采样。
对具有良好动态范围的普通照片(如origin.jpg,均值约 120、标准差约 40)能稳定通过;
对极度均匀/接近纯色的宿主图像(例如均值 > 250/255)可能失败,因为此时水印信号的
相对强度太低,无法经受重采样的低通滤波。这是 DWT-DCT-SVD 盲水印范式的固有局限,
不是本库的 bug。验证缩放鲁棒性请使用origin.jpg作为宿主图。
支持