using System; using System.IO; using OpenCvSharp; namespace Masuit.Tools.DigtalWatermarker; /// /// 数字水印 /// public static class DigitalWatermarker { // 配置:块大小与DCT系数对位置(中频),以及嵌入强度 private const int BlockSize = 8; // 选择两处中频系数位置(行,列),避免(0,0)直流与高频边角 private static readonly (int r, int c) C1 = (2, 3); private static readonly (int r, int c) C2 = (3, 2); // 嵌入强度基准,越大越鲁棒但越易可见;根据图像块能量自适应缩放 private const float Alpha = 12.0f; /// /// 添加数字水印,将水印图片内容隐藏到图像中,实现图像的版权保护和追溯,当图像被修改或攻击时,如裁剪压缩翻转截屏翻录等操作,水印信息不会受到影响。 /// /// 原始图片 /// 水印图片 /// 被水印保护的新图片 public static Mat EmbedWatermark(Mat source, Mat watermark) { if (source.Empty()) throw new ArgumentException("source is empty", nameof(source)); if (watermark.Empty()) throw new ArgumentException("watermark is empty", nameof(watermark)); // 转YCrCb,仅在亮度通道嵌入,最大化视觉不可感知性 Mat srcBgr = source.Clone(); Mat ycrcb = new(); Cv2.CvtColor(srcBgr, ycrcb, ColorConversionCodes.BGR2YCrCb); Mat[] planes = Cv2.Split(ycrcb); Mat y = planes[0]; // 保障处理区域为8的倍数 int w = (y.Cols / BlockSize) * BlockSize; int h = (y.Rows / BlockSize) * BlockSize; var roi = new Rect(0, 0, w, h); Mat yRoi = new(y, roi); // 预处理水印:灰度->二值,缩放到块网格大小(每块嵌入1比特) int gridW = w / BlockSize; int gridH = h / BlockSize; Mat wmGray = watermark.Channels() > 1 ? watermark.CvtColor(ColorConversionCodes.BGR2GRAY) : watermark.Clone(); Mat wmResized = new(); Cv2.Resize(wmGray, wmResized, new Size(gridW, gridH), 0, 0, InterpolationFlags.Area); Mat wmBin = new(); Cv2.Threshold(wmResized, wmBin, 0, 1, ThresholdTypes.Otsu); // 使用32位浮点处理DCT Mat yF = new(); yRoi.ConvertTo(yF, MatType.CV_32F); // 遍历每个8x8块进行嵌入(QIM差值量化,更抗压缩) for (int by = 0; by < gridH; by++) { for (int bx = 0; bx < gridW; bx++) { int bit = wmBin.Get(by, bx) > 0 ? 1 : 0; var blockRect = new Rect(bx * BlockSize, by * BlockSize, BlockSize, BlockSize); using var block = new Mat(yF, blockRect); using Mat dct = block.Clone(); Cv2.Dct(dct, dct); // 自适应阈值尺度(依据块DCT能量) using var absBlock = new Mat(); Cv2.Absdiff(dct, 0, absBlock); double meanAbs = Cv2.Mean(absBlock)[0]; float delta = (float)(Alpha * Math.Max(1.0, meanAbs / 12.0)); float c1 = dct.Get(C1.r, C1.c); float c2 = dct.Get(C2.r, C2.c); float mid = (c1 + c2) / 2f; float diff = c1 - c2; // QIM:将差值量化到间隔为2*delta的格点,并根据bit偏置到 +delta 或 -delta float step = 2f * delta; float q = (float)Math.Round(diff / step); float targetDiff = q * step + (bit == 1 ? +delta : -delta); c1 = mid + targetDiff / 2f; c2 = mid - targetDiff / 2f; dct.Set(C1.r, C1.c, c1); dct.Set(C2.r, C2.c, c2); // 逆DCT写回 Cv2.Dct(dct, block, DctFlags.Inverse); } } // 合并回亮度通道 Mat yU8 = new(); yF.ConvertTo(yU8, MatType.CV_8U); yU8.CopyTo(new Mat(y, roi)); planes[0] = y; // 亮度已更新 Cv2.Merge(planes, ycrcb); Mat result = new(); Cv2.CvtColor(ycrcb, result, ColorConversionCodes.YCrCb2BGR); return result; } /// /// 提取水印内容,从被水印保护的图像中提取隐藏的数字水印信息。 /// /// 被保护的图像 /// 水印内容 public static Mat ExtractWatermark(Mat image) { if (image.Empty()) throw new ArgumentException("image is empty", nameof(image)); // 转Y通道 Mat ycrcb = new(); Cv2.CvtColor(image, ycrcb, ColorConversionCodes.BGR2YCrCb); var planes = Cv2.Split(ycrcb); Mat y = planes[0]; int w = (y.Cols / BlockSize) * BlockSize; int h = (y.Rows / BlockSize) * BlockSize; var roi = new Rect(0, 0, w, h); Mat yRoi = new(y, roi); int gridW = w / BlockSize; int gridH = h / BlockSize; Mat yF = new(); yRoi.ConvertTo(yF, MatType.CV_32F); Mat wm = new(gridH, gridW, MatType.CV_8U); for (int by = 0; by < gridH; by++) { for (int bx = 0; bx < gridW; bx++) { var blockRect = new Rect(bx * BlockSize, by * BlockSize, BlockSize, BlockSize); using var block = new Mat(yF, blockRect); using Mat dct = block.Clone(); Cv2.Dct(dct, dct); float c1 = dct.Get(C1.r, C1.c); float c2 = dct.Get(C2.r, C2.c); byte bit = (byte)(c1 > c2 ? 255 : 0); wm.Set(by, bx, bit); } } // 轻微中值滤波平滑噪声 Mat wmOut = new(); Cv2.MedianBlur(wm, wmOut, 3); return wmOut; } /// /// 从文件路径读取图片并添加数字水印。 /// /// 原始图片路径 /// 水印图片路径 /// 被水印保护的新图片 public static Mat EmbedWatermark(string sourceImagePath, string watermarkImagePath) { if (string.IsNullOrWhiteSpace(sourceImagePath)) throw new ArgumentException("图片路径不能为空", nameof(sourceImagePath)); if (string.IsNullOrWhiteSpace(watermarkImagePath)) throw new ArgumentException("水印图片路径不能为空", nameof(watermarkImagePath)); if (!File.Exists(sourceImagePath)) throw new FileNotFoundException("文件不存在", sourceImagePath); if (!File.Exists(watermarkImagePath)) throw new FileNotFoundException("文件不存在", watermarkImagePath); using var src = Cv2.ImRead(sourceImagePath); using var wm = Cv2.ImRead(watermarkImagePath); return EmbedWatermark(src, wm); } /// /// 从两个流中读取图片并添加数字水印。 /// /// 原始图片流 /// 水印图片流 /// 被水印保护的新图片 public static Mat EmbedWatermark(Stream sourceStream, Stream watermarkStream) { if (sourceStream is null) throw new ArgumentNullException(nameof(sourceStream)); if (watermarkStream is null) throw new ArgumentNullException(nameof(watermarkStream)); using var src = ReadMatFromStream(sourceStream, ImreadModes.Color); using var wm = ReadMatFromStream(watermarkStream, ImreadModes.Color); return EmbedWatermark(src, wm); } /// /// 从文件路径读取图像并提取数字水印。 /// /// 被保护的图像路径 /// 水印内容 public static Mat ExtractWatermark(string imagePath) { if (string.IsNullOrWhiteSpace(imagePath)) throw new ArgumentException("路径不能为空", nameof(imagePath)); if (!File.Exists(imagePath)) throw new FileNotFoundException("文件不存在", imagePath); using var img = Cv2.ImRead(imagePath, ImreadModes.Color); return ExtractWatermark(img); } /// /// 从流中读取图像并提取数字水印。 /// /// 被保护的图像流 /// 水印内容 public static Mat ExtractWatermark(Stream imageStream) { if (imageStream is null) throw new ArgumentNullException(nameof(imageStream)); using var img = ReadMatFromStream(imageStream, ImreadModes.Color); return ExtractWatermark(img); } // 辅助:从 Stream 读取为 Mat private static Mat ReadMatFromStream(Stream stream, ImreadModes mode) { if (stream is null) throw new ArgumentNullException(nameof(stream)); if (!stream.CanRead) throw new ArgumentException("Stream不可用", nameof(stream)); byte[] bytes; if (stream is MemoryStream ms) { bytes = ms.ToArray(); } else { using var tmp = new MemoryStream(); stream.CopyTo(tmp); bytes = tmp.ToArray(); } var img = Cv2.ImDecode(bytes, mode); if (img.Empty()) throw new ArgumentException("stream不能解析为图像", nameof(stream)); return img; } }