Browse Source

重构DCT哈希逻辑并优化图像处理接口

重构了 `ImageHasher` 类的 DCT 哈希计算逻辑,移除了旧版矩阵生成和操作方法,新增模块化私有方法以提升可读性和可维护性。
懒得勤快 3 weeks ago
parent
commit
4f86c2bf6e

+ 1 - 1
BenchmarkTest/BenchmarkTest.csproj

@@ -7,7 +7,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
+    <PackageReference Include="BenchmarkDotNet" Version="0.15.6" />
   </ItemGroup>
 
   <ItemGroup>

+ 1 - 1
Directory.Build.props

@@ -1,6 +1,6 @@
 <Project>
  <PropertyGroup>
-   <Version>2025.5.2</Version>
+   <Version>2025.5.3</Version>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
 </Project>

+ 1 - 1
Masuit.Tools.Abstractions/Masuit.Tools.Abstractions.csproj

@@ -47,7 +47,7 @@
     </ItemGroup>
 
     <ItemGroup>
-        <PackageReference Include="AngleSharp" Version="1.3.0" />
+        <PackageReference Include="AngleSharp" Version="1.3.1" />
         <PackageReference Include="AngleSharp.Css" Version="1.0.0-beta.151" />
         <PackageReference Include="DnsClient" Version="1.8.0" />
         <PackageReference Include="Microsoft.CSharp" Version="4.7.0" />

+ 2 - 0
Masuit.Tools.Abstractions/Media/IImageTransformer.cs

@@ -26,4 +26,6 @@ public interface IImageTransformer
     /// <param name="height">给定高度</param>
     /// <returns>包含转换图像的8位像素值的字节数组。</returns>
     byte[] TransformImage(Image<L8> image, int width, int height);
+
+    public byte[,] GetPixelData(Image<L8> image, int width, int height);
 }

+ 101 - 174
Masuit.Tools.Abstractions/Media/ImageHasher.cs

@@ -523,253 +523,180 @@ public class ImageHasher
     /// 使用DCT算法计算图像的64位哈希
     /// </summary>
     /// <see cref="https://segmentfault.com/a/1190000038308093"/>
-    /// <param name="sourceStream">读取到的图片流</param>
+    /// <param name="path">图片路径</param>
     /// <returns>64位hash值</returns>
-    public ulong DctHash(Stream sourceStream)
+    public ulong DctHash(string path)
     {
-        lock (_dctMatrixLockObject)
-        {
-            if (!_isDctMatrixInitialized)
-            {
-                _dctMatrix = GenerateDctMatrix(32);
-                _isDctMatrixInitialized = true;
-            }
-        }
-
-        var pixels = _transformer.TransformImage(sourceStream, 32, 32);
-
-        // 将像素转换成float类型数组
-        var fPixels = new float[1024];
-        for (var i = 0; i < 1024; i++)
-        {
-            fPixels[i] = pixels[i] / 255.0f;
-        }
-
-        // 计算 dct 矩阵
-        var dctPixels = ComputeDct(fPixels, _dctMatrix);
-
-        // 从矩阵中1,1到8,8获得8x8的区域,忽略最低频率以提高检测
-        var dctHashPixels = new float[64];
-        for (var x = 0; x < 8; x++)
-        {
-            for (var y = 0; y < 8; y++)
-            {
-                dctHashPixels[x + y * 8] = dctPixels[x + 1][y + 1];
-            }
-        }
-
-        // 计算中值
-        var pixelList = new List<float>(dctHashPixels);
-        pixelList.Sort();
-
-        // 中间像素的平均值
-        var median = (pixelList[31] + pixelList[32]) / 2;
+#if NET6_0_OR_GREATER
 
-        // 遍历所有像素,如果超过中值,则将其设置为1,如果低于中值,则将其设置为0。
-        var hash = 0UL;
-        for (var i = 0; i < 64; i++)
+        var decoderOptions = new DecoderOptions
         {
-            if (dctHashPixels[i] > median)
-            {
-                hash |= 1UL << i;
-            }
-        }
-
-        return hash;
+            TargetSize = new Size(160),
+            SkipMetadata = true
+        };
+        using var image = Image.Load<L8>(decoderOptions, path);
+#else
+        using var image = Image.Load<L8>(path);
+#endif
+        return DctHash(image);
     }
 
     /// <summary>
     /// 使用DCT算法计算图像的64位哈希
     /// </summary>
     /// <see cref="https://segmentfault.com/a/1190000038308093"/>
-    /// <param name="image">读取到的图片</param>
+    /// <param name="image">读取到的图片</param>
     /// <returns>64位hash值</returns>
     public ulong DctHash(Image image)
     {
-        using var source = image.CloneAs<L8>();
-        return DctHash(source);
+        using var clone = image.CloneAs<L8>();
+        return DctHash(clone);
     }
 
     /// <summary>
     /// 使用DCT算法计算图像的64位哈希
     /// </summary>
     /// <see cref="https://segmentfault.com/a/1190000038308093"/>
-    /// <param name="image">读取到的图片</param>
+    /// <param name="image">读取到的图片</param>
     /// <returns>64位hash值</returns>
     public ulong DctHash(Image<L8> image)
     {
-        lock (_dctMatrixLockObject)
+        var grayscalePixels = _transformer.GetPixelData(image, 32, 32);
+        var dctMatrix = ComputeDct(grayscalePixels, 32);
+        var topLeftBlock = ExtractTopLeftBlock(dctMatrix, 8);
+        var median = CalculateMedian(topLeftBlock);
+        var hash = GenerateHash(topLeftBlock, median);
+        return hash;
+    }
+
+    /// <summary>
+    /// 计算图像的DCT矩阵
+    /// </summary>
+    /// <param name="input"></param>
+    /// <param name="size"></param>
+    /// <returns></returns>
+    private double[,] ComputeDct(byte[,] input, int size)
+    {
+        var output = new double[size, size];
+        var rowDCT = new double[size, size];
+        for (int y = 0; y < size; y++)
         {
-            if (!_isDctMatrixInitialized)
+            for (int x = 0; x < size; x++)
             {
-                _dctMatrix = GenerateDctMatrix(32);
-                _isDctMatrixInitialized = true;
+                rowDCT[y, x] = input[y, x];
             }
         }
 
-        var pixels = _transformer.TransformImage(image, 32, 32);
-
-        // 将像素转换成float类型数组
-        var fPixels = new float[1024];
-        for (var i = 0; i < 1024; i++)
-        {
-            fPixels[i] = pixels[i] / 255.0f;
-        }
-
-        // 计算 dct 矩阵
-        var dctPixels = ComputeDct(fPixels, _dctMatrix);
-
-        // 从矩阵中1,1到8,8获得8x8的区域,忽略最低频率以提高检测
-        var dctHashPixels = new float[64];
-        for (var x = 0; x < 8; x++)
+        for (int y = 0; y < size; y++)
         {
-            for (var y = 0; y < 8; y++)
+            var row = new double[size];
+            for (int x = 0; x < size; x++)
             {
-                dctHashPixels[x + y * 8] = dctPixels[x + 1][y + 1];
+                row[x] = rowDCT[y, x];
+            }
+            var dctRow = DCT1D(row);
+            for (int x = 0; x < size; x++)
+            {
+                rowDCT[y, x] = dctRow[x];
             }
         }
 
-        // 计算中值
-        var pixelList = new List<float>(dctHashPixels);
-        pixelList.Sort();
-
-        // 中间像素的平均值
-        var median = (pixelList[31] + pixelList[32]) / 2;
-
-        // 遍历所有像素,如果超过中值,则将其设置为1,如果低于中值,则将其设置为0。
-        var hash = 0UL;
-        for (var i = 0; i < 64; i++)
+        for (int x = 0; x < size; x++)
         {
-            if (dctHashPixels[i] > median)
+            var col = new double[size];
+            for (int y = 0; y < size; y++)
             {
-                hash |= 1UL << i;
+                col[y] = rowDCT[y, x];
+            }
+            var dctCol = DCT1D(col);
+            for (int y = 0; y < size; y++)
+            {
+                output[y, x] = dctCol[y];
             }
         }
 
-        return hash;
+        return output;
     }
 
-    /// <summary>
-    /// 使用DCT算法计算图像的64位哈希
-    /// </summary>
-    /// <param name="path">图片的文件路径</param>
-    /// <returns>64位hash值</returns>
-    public ulong DctHash(string path)
+    private double[] DCT1D(double[] input)
     {
-#if NET6_0_OR_GREATER
+        int n = input.Length;
+        var output = new double[n];
 
-        var decoderOptions = new DecoderOptions
+        for (int u = 0; u < n; u++)
         {
-            TargetSize = new Size(128),
-            SkipMetadata = true
-        };
-        using var image = Image.Load<L8>(decoderOptions, path);
-#else
-        using var image = Image.Load<L8>(path);
-#endif
-        return DctHash(image);
-    }
+            double sum = 0.0;
+            double cu = u == 0 ? 1.0 / Math.Sqrt(2.0) : 1.0;
 
-    /// <summary>
-    /// 计算图像的DCT矩阵
-    /// </summary>
-    /// <param name="image">用于计算dct的图像</param>
-    /// <param name="dctMatrix">DCT系数矩阵</param>
-    /// <returns>图像的DCT矩阵</returns>
-    private static float[][] ComputeDct(float[] image, float[][] dctMatrix)
-    {
-        // dct矩阵的大小,图像的大小与DCT矩阵相同
-        var size = dctMatrix.GetLength(0);
+            for (int x = 0; x < n; x++)
+            {
+                sum += input[x] * Math.Cos((Math.PI / n) * (x + 0.5) * u);
+            }
 
-        // 降图像转换成矩阵
-        var imageMat = new float[size][];
-        for (var i = 0; i < size; i++)
-        {
-            imageMat[i] = new float[size];
+            output[u] = cu * sum * Math.Sqrt(2.0 / n);
         }
 
-        for (var y = 0; y < size; y++)
+        return output;
+    }
+
+    private double[,] ExtractTopLeftBlock(double[,] matrix, int blockSize)
+    {
+        var block = new double[blockSize, blockSize];
+
+        for (int y = 0; y < blockSize; y++)
         {
-            for (var x = 0; x < size; x++)
+            for (int x = 0; x < blockSize; x++)
             {
-                imageMat[y][x] = image[x + y * size];
+                block[y, x] = matrix[y, x];
             }
         }
 
-        return Multiply(Multiply(dctMatrix, imageMat), Transpose(dctMatrix));
+        return block;
     }
 
-    /// <summary>
-    /// 生成DCT系数矩阵
-    /// </summary>
-    /// <param name="size">矩阵的大小</param>
-    /// <returns>DCT系数矩阵</returns>
-    private static float[][] GenerateDctMatrix(int size)
+    private double CalculateMedian(double[,] matrix)
     {
-        var matrix = new float[size][];
-        for (int i = 0; i < size; i++)
-        {
-            matrix[i] = new float[size];
-        }
+        int height = matrix.GetLength(0);
+        int width = matrix.GetLength(1);
 
-        var c1 = Math.Sqrt(2.0f / size);
-        for (var j = 0; j < size; j++)
-        {
-            matrix[0][j] = (float)Math.Sqrt(1.0d / size);
-        }
+        var flatArray = new double[height * width];
+        int index = 0;
 
-        for (var j = 0; j < size; j++)
+        for (int y = 0; y < height; y++)
         {
-            for (var i = 1; i < size; i++)
+            for (int x = 0; x < width; x++)
             {
-                matrix[i][j] = (float)(c1 * Math.Cos(((2 * j + 1) * i * Math.PI) / (2.0d * size)));
+                flatArray[index++] = matrix[y, x];
             }
         }
 
-        return matrix;
-    }
+        Array.Sort(flatArray);
 
-    /// <summary>
-    /// 矩阵的乘法运算
-    /// </summary>
-    /// <param name="a">矩阵a</param>
-    /// <param name="b">矩阵b</param>
-    /// <returns>Result matrix.</returns>
-    private static float[][] Multiply(float[][] a, float[][] b)
-    {
-        var n = a[0].Length;
-        var c = new float[n][];
-        for (var i = 0; i < n; i++)
+        if (flatArray.Length % 2 == 0)
         {
-            c[i] = new float[n];
+            return (flatArray[flatArray.Length / 2 - 1] + flatArray[flatArray.Length / 2]) / 2.0;
         }
 
-        for (var i = 0; i < n; i++)
-            for (var k = 0; k < n; k++)
-                for (var j = 0; j < n; j++)
-                    c[i][j] += a[i][k] * b[k][j];
-        return c;
+        return flatArray[flatArray.Length / 2];
     }
 
-    /// <summary>
-    /// 矩阵转置
-    /// </summary>
-    /// <param name="mat">待转换的矩阵</param>
-    /// <returns>转换后的矩阵</returns>
-    private static float[][] Transpose(float[][] mat)
+    private ulong GenerateHash(double[,] block, double median)
     {
-        var size = mat[0].Length;
-        var transpose = new float[size][];
-        for (var i = 0; i < size; i++)
+        ulong hash = 0UL;
+        int bitPosition = 0;
+        for (int y = 0; y < 8; y++)
         {
-            transpose[i] = new float[size];
-            for (var j = 0; j < size; j++)
+            for (int x = 0; x < 8; x++)
             {
-                transpose[i][j] = mat[j][i];
+                if (block[y, x] >= median)
+                {
+                    hash |= (1UL << bitPosition);
+                }
+                bitPosition++;
             }
         }
 
-        return transpose;
+        return hash;
     }
 
     /// <summary>

+ 29 - 0
Masuit.Tools.Abstractions/Media/ImageSharpTransformer.cs

@@ -56,6 +56,35 @@ public class ImageSharpTransformer : IImageTransformer
 
         return bytes;
     }
+
+    public byte[,] GetPixelData(Image<L8> image, int width, int height)
+    {
+        image.Mutate(x => x.Resize(new ResizeOptions()
+        {
+            Size = new Size
+            {
+                Width = width,
+                Height = height
+            },
+            Mode = ResizeMode.Stretch,
+            Sampler = new BicubicResampler()
+        }));
+        var grayscalePixels = new byte[width, height];
+        image.ProcessPixelRows(accessor =>
+        {
+            for (int y = 0; y < width; y++)
+            {
+                var row = accessor.GetRowSpan(y);
+                for (int x = 0; x < height; x++)
+                {
+                    var pixel = row[x];
+                    grayscalePixels[y, x] = pixel.PackedValue;
+                }
+            }
+        });
+
+        return grayscalePixels;
+    }
 }
 
 public static class ImageHashExt

+ 1 - 1
Masuit.Tools/Masuit.Tools.Net.csproj

@@ -35,7 +35,7 @@
 
     <ItemGroup>
         <PackageReference Include="Microsoft.AspNet.Mvc" Version="5.3.0" />
-        <PackageReference Include="StackExchange.Redis" Version="2.9.17" />
+        <PackageReference Include="StackExchange.Redis" Version="2.9.32" />
         <ProjectReference Include="..\Masuit.Tools.Abstractions\Masuit.Tools.Abstractions.csproj" />
         <Reference Include="System.Web" />
     </ItemGroup>

+ 1 - 1
NetCoreTest/NetCoreTest.csproj

@@ -6,7 +6,7 @@
     <ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
   </ItemGroup>
   <ItemGroup>
     <Folder Include="Controllers\" />

+ 2 - 2
Test/Masuit.Tools.Abstractions.Test/Masuit.Tools.Abstractions.Test.csproj

@@ -13,10 +13,10 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
     <PackageReference Include="Moq" Version="4.20.72" />
     <PackageReference Include="xunit" Version="2.9.3" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
+    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>

+ 1 - 1
Test/Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.csproj

@@ -23,7 +23,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
   </ItemGroup>
 
   <ItemGroup>

+ 5 - 5
Test/Masuit.Tools.Core.Test/Masuit.Tools.Core.Test.csproj

@@ -9,12 +9,12 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="9.0.9" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.9" />
-    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
+    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="9.0.10" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.10" />
+    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
     <PackageReference Include="xunit" Version="2.9.3" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
+    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
     </PackageReference>

+ 2 - 2
Test/Masuit.Tools.DigitalWatermarker.Test/Masuit.Tools.DigitalWatermarker.Test.csproj

@@ -14,10 +14,10 @@
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
     <PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
     <PackageReference Include="xunit" Version="2.9.3" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
+    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>

+ 2 - 2
Test/Masuit.Tools.Test/Masuit.Tools.Test.csproj

@@ -94,7 +94,7 @@
       <Version>4.20.72</Version>
     </PackageReference>
     <PackageReference Include="StackExchange.Redis">
-      <Version>2.9.17</Version>
+      <Version>2.9.32</Version>
     </PackageReference>
     <PackageReference Include="System.Runtime.CompilerServices.Unsafe">
       <Version>6.1.2</Version>
@@ -109,7 +109,7 @@
       <Version>2.0.3</Version>
     </PackageReference>
     <PackageReference Include="xunit.analyzers">
-      <Version>1.24.0</Version>
+      <Version>1.25.0</Version>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <PrivateAssets>all</PrivateAssets>
     </PackageReference>