DigitalWatermarker.cs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. using System;
  2. using System.IO;
  3. using OpenCvSharp;
  4. namespace Masuit.Tools.DigtalWatermarker;
  5. /// <summary>
  6. /// 数字水印
  7. /// </summary>
  8. public static class DigitalWatermarker
  9. {
  10. // 配置:块大小与DCT系数对位置(中频),以及嵌入强度
  11. private const int BlockSize = 8;
  12. // 选择两处中频系数位置(行,列),避免(0,0)直流与高频边角
  13. private static readonly (int r, int c) C1 = (2, 3);
  14. private static readonly (int r, int c) C2 = (3, 2);
  15. // 嵌入强度基准,越大越鲁棒但越易可见;根据图像块能量自适应缩放
  16. private const float Alpha = 12.0f;
  17. /// <summary>
  18. /// 添加数字水印,将水印图片内容隐藏到图像中,实现图像的版权保护和追溯,当图像被修改或攻击时,如裁剪压缩翻转截屏翻录等操作,水印信息不会受到影响。
  19. /// </summary>
  20. /// <param name="source">原始图片</param>
  21. /// <param name="watermark">水印图片</param>
  22. /// <returns>被水印保护的新图片</returns>
  23. public static Mat EmbedWatermark(Mat source, Mat watermark)
  24. {
  25. if (source.Empty()) throw new ArgumentException("source is empty", nameof(source));
  26. if (watermark.Empty()) throw new ArgumentException("watermark is empty", nameof(watermark));
  27. // 转YCrCb,仅在亮度通道嵌入,最大化视觉不可感知性
  28. Mat srcBgr = source.Clone();
  29. Mat ycrcb = new();
  30. Cv2.CvtColor(srcBgr, ycrcb, ColorConversionCodes.BGR2YCrCb);
  31. Mat[] planes = Cv2.Split(ycrcb);
  32. Mat y = planes[0];
  33. // 保障处理区域为8的倍数
  34. int w = (y.Cols / BlockSize) * BlockSize;
  35. int h = (y.Rows / BlockSize) * BlockSize;
  36. var roi = new Rect(0, 0, w, h);
  37. Mat yRoi = new(y, roi);
  38. // 预处理水印:灰度->二值,缩放到块网格大小(每块嵌入1比特)
  39. int gridW = w / BlockSize;
  40. int gridH = h / BlockSize;
  41. Mat wmGray = watermark.Channels() > 1 ? watermark.CvtColor(ColorConversionCodes.BGR2GRAY) : watermark.Clone();
  42. Mat wmResized = new();
  43. Cv2.Resize(wmGray, wmResized, new Size(gridW, gridH), 0, 0, InterpolationFlags.Area);
  44. Mat wmBin = new();
  45. Cv2.Threshold(wmResized, wmBin, 0, 1, ThresholdTypes.Otsu);
  46. // 使用32位浮点处理DCT
  47. Mat yF = new();
  48. yRoi.ConvertTo(yF, MatType.CV_32F);
  49. // 遍历每个8x8块进行嵌入(QIM差值量化,更抗压缩)
  50. for (int by = 0; by < gridH; by++)
  51. {
  52. for (int bx = 0; bx < gridW; bx++)
  53. {
  54. int bit = wmBin.Get<byte>(by, bx) > 0 ? 1 : 0;
  55. var blockRect = new Rect(bx * BlockSize, by * BlockSize, BlockSize, BlockSize);
  56. using var block = new Mat(yF, blockRect);
  57. using Mat dct = block.Clone();
  58. Cv2.Dct(dct, dct);
  59. // 自适应阈值尺度(依据块DCT能量)
  60. using var absBlock = new Mat();
  61. Cv2.Absdiff(dct, 0, absBlock);
  62. double meanAbs = Cv2.Mean(absBlock)[0];
  63. float delta = (float)(Alpha * Math.Max(1.0, meanAbs / 12.0));
  64. float c1 = dct.Get<float>(C1.r, C1.c);
  65. float c2 = dct.Get<float>(C2.r, C2.c);
  66. float mid = (c1 + c2) / 2f;
  67. float diff = c1 - c2;
  68. // QIM:将差值量化到间隔为2*delta的格点,并根据bit偏置到 +delta 或 -delta
  69. float step = 2f * delta;
  70. float q = (float)Math.Round(diff / step);
  71. float targetDiff = q * step + (bit == 1 ? +delta : -delta);
  72. c1 = mid + targetDiff / 2f;
  73. c2 = mid - targetDiff / 2f;
  74. dct.Set(C1.r, C1.c, c1);
  75. dct.Set(C2.r, C2.c, c2);
  76. // 逆DCT写回
  77. Cv2.Dct(dct, block, DctFlags.Inverse);
  78. }
  79. }
  80. // 合并回亮度通道
  81. Mat yU8 = new();
  82. yF.ConvertTo(yU8, MatType.CV_8U);
  83. yU8.CopyTo(new Mat(y, roi));
  84. planes[0] = y; // 亮度已更新
  85. Cv2.Merge(planes, ycrcb);
  86. Mat result = new();
  87. Cv2.CvtColor(ycrcb, result, ColorConversionCodes.YCrCb2BGR);
  88. return result;
  89. }
  90. /// <summary>
  91. /// 提取水印内容,从被水印保护的图像中提取隐藏的数字水印信息。
  92. /// </summary>
  93. /// <param name="image">被保护的图像</param>
  94. /// <returns>水印内容</returns>
  95. public static Mat ExtractWatermark(Mat image)
  96. {
  97. if (image.Empty()) throw new ArgumentException("image is empty", nameof(image));
  98. // 转Y通道
  99. Mat ycrcb = new();
  100. Cv2.CvtColor(image, ycrcb, ColorConversionCodes.BGR2YCrCb);
  101. var planes = Cv2.Split(ycrcb);
  102. Mat y = planes[0];
  103. int w = (y.Cols / BlockSize) * BlockSize;
  104. int h = (y.Rows / BlockSize) * BlockSize;
  105. var roi = new Rect(0, 0, w, h);
  106. Mat yRoi = new(y, roi);
  107. int gridW = w / BlockSize;
  108. int gridH = h / BlockSize;
  109. Mat yF = new();
  110. yRoi.ConvertTo(yF, MatType.CV_32F);
  111. Mat wm = new(gridH, gridW, MatType.CV_8U);
  112. for (int by = 0; by < gridH; by++)
  113. {
  114. for (int bx = 0; bx < gridW; bx++)
  115. {
  116. var blockRect = new Rect(bx * BlockSize, by * BlockSize, BlockSize, BlockSize);
  117. using var block = new Mat(yF, blockRect);
  118. using Mat dct = block.Clone();
  119. Cv2.Dct(dct, dct);
  120. float c1 = dct.Get<float>(C1.r, C1.c);
  121. float c2 = dct.Get<float>(C2.r, C2.c);
  122. byte bit = (byte)(c1 > c2 ? 255 : 0);
  123. wm.Set(by, bx, bit);
  124. }
  125. }
  126. // 轻微中值滤波平滑噪声
  127. Mat wmOut = new();
  128. Cv2.MedianBlur(wm, wmOut, 3);
  129. return wmOut;
  130. }
  131. /// <summary>
  132. /// 从文件路径读取图片并添加数字水印。
  133. /// </summary>
  134. /// <param name="sourceImagePath">原始图片路径</param>
  135. /// <param name="watermarkImagePath">水印图片路径</param>
  136. /// <returns>被水印保护的新图片</returns>
  137. public static Mat EmbedWatermark(string sourceImagePath, string watermarkImagePath)
  138. {
  139. if (string.IsNullOrWhiteSpace(sourceImagePath)) throw new ArgumentException("图片路径不能为空", nameof(sourceImagePath));
  140. if (string.IsNullOrWhiteSpace(watermarkImagePath)) throw new ArgumentException("水印图片路径不能为空", nameof(watermarkImagePath));
  141. if (!File.Exists(sourceImagePath)) throw new FileNotFoundException("文件不存在", sourceImagePath);
  142. if (!File.Exists(watermarkImagePath)) throw new FileNotFoundException("文件不存在", watermarkImagePath);
  143. using var src = Cv2.ImRead(sourceImagePath);
  144. using var wm = Cv2.ImRead(watermarkImagePath);
  145. return EmbedWatermark(src, wm);
  146. }
  147. /// <summary>
  148. /// 从两个流中读取图片并添加数字水印。
  149. /// </summary>
  150. /// <param name="sourceStream">原始图片流</param>
  151. /// <param name="watermarkStream">水印图片流</param>
  152. /// <returns>被水印保护的新图片</returns>
  153. public static Mat EmbedWatermark(Stream sourceStream, Stream watermarkStream)
  154. {
  155. if (sourceStream is null) throw new ArgumentNullException(nameof(sourceStream));
  156. if (watermarkStream is null) throw new ArgumentNullException(nameof(watermarkStream));
  157. using var src = ReadMatFromStream(sourceStream, ImreadModes.Color);
  158. using var wm = ReadMatFromStream(watermarkStream, ImreadModes.Color);
  159. return EmbedWatermark(src, wm);
  160. }
  161. /// <summary>
  162. /// 从文件路径读取图像并提取数字水印。
  163. /// </summary>
  164. /// <param name="imagePath">被保护的图像路径</param>
  165. /// <returns>水印内容</returns>
  166. public static Mat ExtractWatermark(string imagePath)
  167. {
  168. if (string.IsNullOrWhiteSpace(imagePath)) throw new ArgumentException("路径不能为空", nameof(imagePath));
  169. if (!File.Exists(imagePath)) throw new FileNotFoundException("文件不存在", imagePath);
  170. using var img = Cv2.ImRead(imagePath, ImreadModes.Color);
  171. return ExtractWatermark(img);
  172. }
  173. /// <summary>
  174. /// 从流中读取图像并提取数字水印。
  175. /// </summary>
  176. /// <param name="imageStream">被保护的图像流</param>
  177. /// <returns>水印内容</returns>
  178. public static Mat ExtractWatermark(Stream imageStream)
  179. {
  180. if (imageStream is null) throw new ArgumentNullException(nameof(imageStream));
  181. using var img = ReadMatFromStream(imageStream, ImreadModes.Color);
  182. return ExtractWatermark(img);
  183. }
  184. // 辅助:从 Stream 读取为 Mat
  185. private static Mat ReadMatFromStream(Stream stream, ImreadModes mode)
  186. {
  187. if (stream is null) throw new ArgumentNullException(nameof(stream));
  188. if (!stream.CanRead) throw new ArgumentException("Stream不可用", nameof(stream));
  189. byte[] bytes;
  190. if (stream is MemoryStream ms)
  191. {
  192. bytes = ms.ToArray();
  193. }
  194. else
  195. {
  196. using var tmp = new MemoryStream();
  197. stream.CopyTo(tmp);
  198. bytes = tmp.ToArray();
  199. }
  200. var img = Cv2.ImDecode(bytes, mode);
  201. if (img.Empty())
  202. throw new ArgumentException("stream不能解析为图像", nameof(stream));
  203. return img;
  204. }
  205. }