Browse Source

新增图像添加盲水印工具包

懒得勤快 1 tháng trước cách đây
mục cha
commit
a4588e8fc7

+ 243 - 0
Masuit.Tools.DigtalWatermarker/DigitalWatermarker.cs

@@ -0,0 +1,243 @@
+using System;
+using System.IO;
+using OpenCvSharp;
+
+namespace Masuit.Tools.DigtalWatermarker;
+
+/// <summary>
+/// 数字水印
+/// </summary>
+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;
+
+    /// <summary>
+    /// 添加数字水印,将水印图片内容隐藏到图像中,实现图像的版权保护和追溯,当图像被修改或攻击时,如裁剪压缩翻转截屏翻录等操作,水印信息不会受到影响。
+    /// </summary>
+    /// <param name="source">原始图片</param>
+    /// <param name="watermark">水印图片</param>
+    /// <returns>被水印保护的新图片</returns>
+    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<byte>(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<float>(C1.r, C1.c);
+                float c2 = dct.Get<float>(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;
+    }
+
+    /// <summary>
+    /// 提取水印内容,从被水印保护的图像中提取隐藏的数字水印信息。
+    /// </summary>
+    /// <param name="image">被保护的图像</param>
+    /// <returns>水印内容</returns>
+    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<float>(C1.r, C1.c);
+                float c2 = dct.Get<float>(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;
+    }
+
+    /// <summary>
+    /// 从文件路径读取图片并添加数字水印。
+    /// </summary>
+    /// <param name="sourceImagePath">原始图片路径</param>
+    /// <param name="watermarkImagePath">水印图片路径</param>
+    /// <returns>被水印保护的新图片</returns>
+    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);
+    }
+
+    /// <summary>
+    /// 从两个流中读取图片并添加数字水印。
+    /// </summary>
+    /// <param name="sourceStream">原始图片流</param>
+    /// <param name="watermarkStream">水印图片流</param>
+    /// <returns>被水印保护的新图片</returns>
+    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);
+    }
+
+    /// <summary>
+    /// 从文件路径读取图像并提取数字水印。
+    /// </summary>
+    /// <param name="imagePath">被保护的图像路径</param>
+    /// <returns>水印内容</returns>
+    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);
+    }
+
+    /// <summary>
+    /// 从流中读取图像并提取数字水印。
+    /// </summary>
+    /// <param name="imageStream">被保护的图像流</param>
+    /// <returns>水印内容</returns>
+    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;
+    }
+}

+ 43 - 0
Masuit.Tools.DigtalWatermarker/Masuit.Tools.DigtalWatermarker.csproj

@@ -0,0 +1,43 @@
+<Project Sdk="Microsoft.NET.Sdk">
+    <PropertyGroup>
+        <TargetFramework>netstandard2.0</TargetFramework>
+        <LangVersion>latest</LangVersion>
+        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+        <Authors>懒得勤快</Authors>
+        <Description>Masuit.Tools.DigtalWatermarker数字水印库</Description>
+        <Copyright>懒得勤快</Copyright>
+        <RepositoryUrl>https://github.com/ldqk/Masuit.Tools</RepositoryUrl>
+        <PackageProjectUrl>https://github.com/ldqk/Masuit.Tools</PackageProjectUrl>
+        <PackageTags>Masuit.Tools,工具库,Utility,OpenCV,Extensions</PackageTags>
+        <PackageReleaseNotes>Masuit.Tools.DigtalWatermarker数字水印库</PackageReleaseNotes>
+        <Product>Masuit.Tools.DigtalWatermarker</Product>
+        <PackageId>Masuit.Tools.DigtalWatermarker</PackageId>
+        <RepositoryType>Github</RepositoryType>
+        <GeneratePackageOnBuild>True</GeneratePackageOnBuild>
+        <PackageRequireLicenseAcceptance>False</PackageRequireLicenseAcceptance>
+        <FileVersion>1.0</FileVersion>
+        <Company>masuit.org</Company>
+        <AssemblyVersion>1.0</AssemblyVersion>
+        <PackageLicenseUrl>https://github.com/ldqk/Masuit.Tools/blob/master/LICENSE</PackageLicenseUrl>
+        <EmbedUntrackedSources>true</EmbedUntrackedSources>
+        <IncludeSymbols>true</IncludeSymbols>
+        <SymbolPackageFormat>snupkg</SymbolPackageFormat>
+        <GenerateDocumentationFile>True</GenerateDocumentationFile>
+        <Title>Masuit.Tools.DigtalWatermarker</Title>
+        <PackageReadmeFile>README-DigitalWatermarker.md</PackageReadmeFile>
+        <NeutralLanguage>zh-CN</NeutralLanguage>
+    </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Update="README-DigitalWatermarker.md">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+      <Pack>True</Pack>
+      <PackagePath>\</PackagePath>
+    </None>
+  </ItemGroup>
+
+</Project>

+ 161 - 0
Masuit.Tools.DigtalWatermarker/README-DigitalWatermarker.md

@@ -0,0 +1,161 @@
+# Masuit.Tools.DigitalWatermarker
+## 简介
+
+Masuit.Tools.DigitalWatermarker 是一个基于 OpenCV 的数字水印库,提供鲁棒的图像数字水印嵌入和提取功能。该库采用 DCT(离散余弦变换)和 QIM(量化索引调制)技术,可以在图像中嵌入不可见的数字水印,用于版权保护和内容追溯。
+
+## 特性
+
+- **不可见水印**:水印嵌入在图像的频域中,肉眼不可见
+- **鲁棒性强**:对图像压缩、裁剪、翻转、翻录等攻击具有较强的抵抗能力
+- **自适应强度**:根据图像块的能量自适应调整嵌入强度
+- **简单易用**:提供简洁的 API,支持文件路径、流等多种输入方式
+- **高性能**:基于 OpenCV 实现,处理速度快
+
+## 技术原理
+
+该库采用以下关键技术:
+
+1. **DCT变换**:在 8×8 像素块上进行离散余弦变换
+2. **中频嵌入**:选择中频系数位置 (2,3) 和 (3,2) 进行水印嵌入
+3. **QIM调制**:使用量化索引调制技术,提高抗压缩能力
+4. **YCrCb色彩空间**:仅在亮度通道嵌入,最大化视觉不可感知性
+5. **自适应阈值**:根据图像块的 DCT 能量自适应调整嵌入强度
+
+## 安装
+
+通过 NuGet 安装:
+
+```bash
+Install-Package Masuit.Tools.DigitalWatermarker
+Install-Package OpenCvSharp4.runtime.win
+```
+
+或者使用 .NET CLI:
+
+```bash
+dotnet add package Masuit.Tools.DigitalWatermarker
+dotnet add package OpenCvSharp4.runtime.win
+```
+
+## 使用方法
+
+### 基本用法
+
+```csharp
+using Masuit.Tools.DigtalWatermarker;
+using OpenCvSharp;
+
+// 从文件嵌入水印
+Mat watermarkedImage = DigitalWatermarker.EmbedWatermark("source.jpg", "watermark.png");
+Cv2.ImWrite("watermarked.jpg", watermarkedImage);
+
+// 提取水印
+Mat extractedWatermark = DigitalWatermarker.ExtractWatermark("watermarked.jpg");
+Cv2.ImWrite("extracted_watermark.png", extractedWatermark);
+```
+
+### 使用 Mat 对象
+
+```csharp
+using var source = Cv2.ImRead("source.jpg");
+using var watermark = Cv2.ImRead("watermark.png");
+
+// 嵌入水印
+using var watermarked = DigitalWatermarker.EmbedWatermark(source, watermark);
+Cv2.ImWrite("result.jpg", watermarked);
+
+// 提取水印
+using var extracted = DigitalWatermarker.ExtractWatermark(watermarked);
+Cv2.ImWrite("extracted.png", extracted);
+```
+
+### 使用流
+
+```csharp
+using var sourceStream = File.OpenRead("source.jpg");
+using var watermarkStream = File.OpenRead("watermark.png");
+
+// 嵌入水印
+using var watermarked = DigitalWatermarker.EmbedWatermark(sourceStream, watermarkStream);
+Cv2.ImWrite("result.jpg", watermarked);
+
+// 从流提取水印
+using var imageStream = File.OpenRead("watermarked.jpg");
+using var extracted = DigitalWatermarker.ExtractWatermark(imageStream);
+Cv2.ImWrite("extracted.png", extracted);
+```
+
+## 最佳实践
+
+### 水印图像要求
+
+1. **尺寸建议**:水印图像会被自动缩放到适合的尺寸(基于 8×8 块网格)
+2. **内容建议**:使用高对比度的黑白图像作为水印,效果更佳
+3. **格式支持**:支持所有 OpenCV 支持的图像格式
+
+### 使用建议
+
+1. **源图像质量**:建议使用高质量的源图像,避免过度压缩
+2. **水印提取**:提取的水印可能包含噪声,可以进行后处理优化
+3. **鲁棒性测试**:建议对嵌入水印的图像进行各种攻击测试,验证水印的鲁棒性
+
+### 性能优化
+
+1. **图像尺寸**:处理时间与图像尺寸成正比,大图像需要更多处理时间
+2. **内存管理**:及时释放 Mat 对象以避免内存泄漏
+3. **批量处理**:如需处理大量图像,建议进行并行处理
+
+## 应用场景
+
+### 版权保护
+
+```csharp
+// 为摄影作品添加版权水印
+var photographer = "© 2024 Photographer Name";
+var logo = Cv2.ImRead("logo.png");
+var photo = Cv2.ImRead("photo.jpg");
+
+using var protected_photo = DigitalWatermarker.EmbedWatermark(photo, logo);
+Cv2.ImWrite("protected_photo.jpg", protected_photo);
+```
+
+### 内容溯源
+
+```csharp
+// 为媒体内容添加来源标识
+var sourceId = GenerateSourceIdentifier(); // 生成唯一标识图像
+var content = Cv2.ImRead("content.jpg");
+
+using var traced_content = DigitalWatermarker.EmbedWatermark(content, sourceId);
+// 发布traced_content...
+
+// 后续验证来源
+using var extracted_id = DigitalWatermarker.ExtractWatermark(traced_content);
+bool isAuthentic = VerifySourceIdentifier(extracted_id);
+```
+
+## 限制和注意事项
+
+1. **处理区域**:只处理图像中8的倍数的区域,边缘部分可能被忽略
+2. **色彩空间**:仅在亮度通道进行水印嵌入
+3. **OpenCV依赖**:需要安装 OpenCV 运行时
+4. **攻击抵抗**:虽然具有一定的鲁棒性,但不能抵抗所有类型的攻击
+
+## 系统要求
+
+- .NET Standard 2.0 或更高版本
+- OpenCvSharp4 (自动安装)
+
+## 许可证
+
+本项目采用 MIT 许可证。详情请参见 [LICENSE](https://github.com/ldqk/Masuit.Tools/blob/master/LICENSE) 文件。
+
+## 贡献
+
+欢迎提交 Issue 和 Pull Request!
+
+## 相关链接
+
+- [项目主页](https://github.com/ldqk/Masuit.Tools)
+- [NuGet 包](https://www.nuget.org/packages/Masuit.Tools.DigitalWatermarker)
+- [问题反馈](https://github.com/ldqk/Masuit.Tools/issues)

+ 7 - 1
Masuit.Tools.sln

@@ -1,6 +1,6 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
-# 17
+# Visual Studio Version 17
 VisualStudioVersion = 17.1.32210.238
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masuit.Tools.Net", "Masuit.Tools\Masuit.Tools.Net.csproj", "{275D5A0D-C49C-497E-A4B5-F40285C2495F}"
@@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetCoreTest", "NetCoreTest\
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkTest", "BenchmarkTest\BenchmarkTest.csproj", "{0599ACF0-8495-4E72-AB5E-B7446A5C413A}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masuit.Tools.DigtalWatermarker", "Masuit.Tools.DigtalWatermarker\Masuit.Tools.DigtalWatermarker.csproj", "{2CB82B76-8F91-4826-A40B-B30FBB9A5108}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -89,6 +91,10 @@ Global
 		{0599ACF0-8495-4E72-AB5E-B7446A5C413A}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{0599ACF0-8495-4E72-AB5E-B7446A5C413A}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{0599ACF0-8495-4E72-AB5E-B7446A5C413A}.Release|Any CPU.Build.0 = Release|Any CPU
+		{2CB82B76-8F91-4826-A40B-B30FBB9A5108}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{2CB82B76-8F91-4826-A40B-B30FBB9A5108}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2CB82B76-8F91-4826-A40B-B30FBB9A5108}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{2CB82B76-8F91-4826-A40B-B30FBB9A5108}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 7 - 0
README.md

@@ -774,6 +774,13 @@ var sim=ImageHasher.Compare(hash1,hash2); // 图片的相似度,范围:[0,1]
 var imageFormat=stream.GetImageType(); // 获取图片的真实格式
 ```
 最佳实践案例,以图搜图:https://github.com/ldqk/ImageSearch
+
+#### 图像添加盲水印
+```bash
+Install-Package Masuit.Tools.DigitalWatermarker
+Install-Package OpenCvSharp4.runtime.win
+```
+用法参阅:[Masuit.Tools.DigitalWatermarker](./Masuit.Tools.DigitalWatermarker/README-DigitalWatermarker.md)
 ### 26.随机数
 
 ```csharp