一、原理
盲数字水印(Blind Watermarking)指提取水印时无需原始载体图像,仅依靠含水印图像和密钥即可完成。
DWT(离散小波变换) 将图像分解为:
- LL:低频近似分量(能量集中,稳定性强,适合嵌入水印)
- LH/HL/HH:高频细节分量(边缘/纹理,不适合嵌入,易失真)
嵌入逻辑 :修改 LL 分量的系数(如奇偶性、幅度偏移),隐藏水印信息;
提取逻辑:对含水印图像做相同 DWT 分解,从 LL 分量反推水印信息。
二、完整 C# 实现(使用 AForge.NET)
2.1 环境配置
xml
<!-- NuGet 依赖 -->
<PackageReference Include="AForge.Imaging" Version="2.2.5" />
<PackageReference Include="AForge.Math" Version="2.2.5" />
2.2 核心类:DwtBlindWatermark.cs
csharp
using AForge.Imaging;
using AForge.Imaging.Filters;
using AForge.Math;
using System;
using System.Drawing;
using System.Drawing.Imaging;
namespace DwtWatermark
{
public class DwtBlindWatermark
{
// 配置参数(嵌入/提取必须一致!)
private const string WaveletName = "haar"; // 小波基(haar 计算最快)
private const int DecompositionLevels = 1; // DWT 分解层数(1层足够)
private const double EmbedStrength = 0.05; // 嵌入强度(0.01~0.1,平衡不可见性)
private const int WatermarkSize = 32; // 水印尺寸(32x32 二值图)
/// <summary>
/// 嵌入水印(盲水印:仅修改 LL 分量)
/// </summary>
public Bitmap EmbedWatermark(Bitmap carrier, Bitmap watermark, int secretKey)
{
// 1. 预处理:转为灰度图
var grayCarrier = Grayscale.CommonAlgorithms.BT709.Apply(carrier);
var grayWatermark = Grayscale.CommonAlgorithms.BT709.Apply(watermark);
// 2. 载体 DWT 分解
ComplexImage complexCarrier = ComplexImage.FromBitmap(grayCarrier);
complexCarrier.ForwardDWT(WaveletName, DecompositionLevels);
Complex[,] ll = complexCarrier.Image; // LL 分量(复数数组,取实部)
// 3. 水印二值化并转为序列
bool[,] watermarkBits = BinarizeWatermark(grayWatermark);
int bitIndex = 0;
// 4. 嵌入水印(修改 LL 系数的奇偶性)
Random rnd = new Random(secretKey);
int stride = ll.GetLength(1);
for (int y = 0; y < ll.GetLength(0); y++)
{
for (int x = 0; x < ll.GetLength(1); x++)
{
if (bitIndex >= WatermarkSize * WatermarkSize) break;
// 随机选择 LL 系数(增强安全性)
int rx = rnd.Next(ll.GetLength(0));
int ry = rnd.Next(ll.GetLength(1));
double coeff = ll[rx, ry].Re;
// 嵌入规则:水印位 1 → 系数调整为奇数;0 → 偶数
bool watermarkBit = watermarkBits[bitIndex / WatermarkSize, bitIndex % WatermarkSize];
double modifiedCoeff = coeff;
if (watermarkBit)
modifiedCoeff = Math.Ceiling(coeff) % 2 == 0 ? coeff + EmbedStrength : coeff;
else
modifiedCoeff = Math.Floor(coeff) % 2 == 1 ? coeff - EmbedStrength : coeff;
ll[rx, ry] = new Complex(modifiedCoeff, ll[rx, ry].Im);
bitIndex++;
}
}
// 5. 逆 DWT 重构含水印图像
complexCarrier.BackwardDWT(WaveletName, DecompositionLevels);
return complexCarrier.ToBitmap();
}
/// <summary>
/// 提取水印(无需原始载体)
/// </summary>
public Bitmap ExtractWatermark(Bitmap watermarkedCarrier, int secretKey)
{
// 1. 预处理
var grayCarrier = Grayscale.CommonAlgorithms.BT709.Apply(watermarkedCarrier);
// 2. 含水印图像 DWT 分解
ComplexImage complexCarrier = ComplexImage.FromBitmap(grayCarrier);
complexCarrier.ForwardDWT(WaveletName, DecompositionLevels);
Complex[,] ll = complexCarrier.Image;
// 3. 提取水印比特
bool[,] extractedBits = new bool[WatermarkSize, WatermarkSize];
Random rnd = new Random(secretKey);
int bitIndex = 0;
for (int y = 0; y < ll.GetLength(0); y++)
{
for (int x = 0; x < ll.GetLength(1); x++)
{
if (bitIndex >= WatermarkSize * WatermarkSize) break;
int rx = rnd.Next(ll.GetLength(0));
int ry = rnd.Next(ll.GetLength(1));
double coeff = ll[rx, ry].Re;
// 提取规则:系数为奇数 → 1;偶数 → 0
extractedBits[bitIndex / WatermarkSize, bitIndex % WatermarkSize] =
Math.Abs(coeff % 2) > 0.5;
bitIndex++;
}
}
// 4. 比特数组转水印图像
return BitsToBitmap(extractedBits);
}
#region 辅助方法
// 水印二值化(阈值 128)
private bool[,] BinarizeWatermark(Bitmap watermark)
{
bool[,] bits = new bool[WatermarkSize, WatermarkSize];
Bitmap resized = new ResizeNearestNeighbor(WatermarkSize, WatermarkSize).Apply(watermark);
for (int y = 0; y < WatermarkSize; y++)
for (int x = 0; x < WatermarkSize; x++)
bits[y, x] = resized.GetPixel(x, y).R > 128;
return bits;
}
// 比特数组转图像
private Bitmap BitsToBitmap(bool[,] bits)
{
Bitmap bmp = new Bitmap(WatermarkSize, WatermarkSize);
for (int y = 0; y < WatermarkSize; y++)
for (int x = 0; x < WatermarkSize; x++)
bmp.SetPixel(x, y, bits[y, x] ? Color.White : Color.Black);
return bmp;
}
#endregion
}
}
2.3 主程序测试(Program.cs)
csharp
using System;
using System.Drawing;
using System.Drawing.Imaging;
namespace DwtWatermark
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("=== DWT 盲数字水印测试 ===\n");
// 1. 读取图像(替换为你的路径)
Bitmap carrier = new Bitmap("carrier.jpg"); // 载体图像(如风景照)
Bitmap watermark = new Bitmap("logo.png"); // 水印图像(32x32 二值图)
int secretKey = 123456; // 密钥(嵌入/提取必须一致!)
var watermarker = new DwtBlindWatermark();
// 2. 嵌入水印
Console.WriteLine("嵌入水印...");
Bitmap watermarked = watermarker.EmbedWatermark(carrier, watermark, secretKey);
watermarked.Save("watermarked.jpg", ImageFormat.Jpeg);
Console.WriteLine("含水印图像保存至:watermarked.jpg\n");
// 3. 提取水印
Console.WriteLine("提取水印...");
Bitmap extracted = watermarker.ExtractWatermark(watermarked, secretKey);
extracted.Save("extracted_watermark.jpg", ImageFormat.Jpeg);
Console.WriteLine("提取的水印保存至:extracted_watermark.jpg\n");
// 4. 评估(PSNR:峰值信噪比,>30dB 说明不可见性好)
double psnr = CalculatePSNR(carrier, watermarked);
Console.WriteLine($"载体与含水印图像的 PSNR: {psnr:F2} dB");
Console.WriteLine("(PSNR > 30dB 表示水印不可见)");
Console.WriteLine("\n测试完成!按任意键退出...");
Console.ReadKey();
}
// 计算 PSNR(评估不可见性)
static double CalculatePSNR(Bitmap original, Bitmap compressed)
{
double mse = 0;
for (int y = 0; y < original.Height; y++)
{
for (int x = 0; x < original.Width; x++)
{
Color c1 = original.GetPixel(x, y);
Color c2 = compressed.GetPixel(x, y);
mse += Math.Pow(c1.R - c2.R, 2) + Math.Pow(c1.G - c2.G, 2) + Math.Pow(c1.B - c2.B, 2);
}
}
mse /= (original.Width * original.Height * 3);
return 10 * Math.Log10(255 * 255 / mse);
}
}
}
三、参数说明
| 参数 | 作用 | 建议值 |
|---|---|---|
WaveletName |
小波基 | haar(计算快,稳定性好) |
DecompositionLevels |
DWT 分解层数 | 1(层数过多易失真) |
EmbedStrength |
嵌入强度 | 0.01~0.1(越大鲁棒性越强,但易失真) |
secretKey |
密钥 | 整数(嵌入/提取必须一致) |
参考 盲水印嵌入提取 www.youwenfan.com/contentcsu/63122.html
四、鲁棒性测试
对含水印图像进行以下攻击,验证水印是否可提取:
- JPEG 压缩(质量 50%)→ 水印应仍可识别;
- 高斯噪声(均值 0,方差 0.01)→ 轻微影响;
- 裁剪(裁剪 10% 边缘)→ 依赖 LL 分量,仍可能提取;
- 旋转(±5°)→ 需结合几何校正(本示例未实现,可扩展)。