Skip to content

Commit e7a2ff1

Browse files
committed
加上文本库的视觉比较器
1 parent 22a0df2 commit e7a2ff1

6 files changed

Lines changed: 286 additions & 0 deletions

File tree

src/MediaConverters/MediaConverters.sln

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{208BA388-4
1515
README.zh-cn.md = README.zh-cn.md
1616
EndProjectSection
1717
EndProject
18+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
19+
EndProject
20+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TextVisionComparer", "TextVisionComparer\TextVisionComparer.csproj", "{3F0E99D4-F28E-349C-D16D-7517B102104A}"
21+
EndProject
1822
Global
1923
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2024
Debug|Any CPU = Debug|Any CPU
@@ -61,10 +65,26 @@ Global
6165
{DEC0E07F-8A6B-4D0E-A44A-8AC45214D95A}.Release|x64.Build.0 = Release|Any CPU
6266
{DEC0E07F-8A6B-4D0E-A44A-8AC45214D95A}.Release|x86.ActiveCfg = Release|Any CPU
6367
{DEC0E07F-8A6B-4D0E-A44A-8AC45214D95A}.Release|x86.Build.0 = Release|Any CPU
68+
{3F0E99D4-F28E-349C-D16D-7517B102104A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
69+
{3F0E99D4-F28E-349C-D16D-7517B102104A}.Debug|Any CPU.Build.0 = Debug|Any CPU
70+
{3F0E99D4-F28E-349C-D16D-7517B102104A}.Debug|x64.ActiveCfg = Debug|Any CPU
71+
{3F0E99D4-F28E-349C-D16D-7517B102104A}.Debug|x64.Build.0 = Debug|Any CPU
72+
{3F0E99D4-F28E-349C-D16D-7517B102104A}.Debug|x86.ActiveCfg = Debug|Any CPU
73+
{3F0E99D4-F28E-349C-D16D-7517B102104A}.Debug|x86.Build.0 = Debug|Any CPU
74+
{3F0E99D4-F28E-349C-D16D-7517B102104A}.Release|Any CPU.ActiveCfg = Release|Any CPU
75+
{3F0E99D4-F28E-349C-D16D-7517B102104A}.Release|Any CPU.Build.0 = Release|Any CPU
76+
{3F0E99D4-F28E-349C-D16D-7517B102104A}.Release|x64.ActiveCfg = Release|Any CPU
77+
{3F0E99D4-F28E-349C-D16D-7517B102104A}.Release|x64.Build.0 = Release|Any CPU
78+
{3F0E99D4-F28E-349C-D16D-7517B102104A}.Release|x86.ActiveCfg = Release|Any CPU
79+
{3F0E99D4-F28E-349C-D16D-7517B102104A}.Release|x86.Build.0 = Release|Any CPU
6480
EndGlobalSection
6581
GlobalSection(SolutionProperties) = preSolution
6682
HideSolutionNode = FALSE
6783
EndGlobalSection
84+
GlobalSection(NestedProjects) = preSolution
85+
{DEC0E07F-8A6B-4D0E-A44A-8AC45214D95A} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
86+
{3F0E99D4-F28E-349C-D16D-7517B102104A} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
87+
EndGlobalSection
6888
GlobalSection(ExtensibilityGlobals) = postSolution
6989
SolutionGuid = {CDD047F3-A4C1-40F8-B7E1-F22E77C16B4D}
7090
EndGlobalSection
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// See https://aka.ms/new-console-template for more information
2+
3+
using TextVisionComparer;
4+
5+
VisionComparer visionComparer = new VisionComparer();
6+
var result = visionComparer.Compare(new FileInfo("4a15iio0.50f.png"), new FileInfo("v31zohcv.ri3.png"));
7+
result.IsSimilar();
8+
9+
Console.WriteLine("Hello, World!");
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFrameworks>net9.0;net8.0;net6.0</TargetFrameworks>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<GenerateDocumentationFile>false</GenerateDocumentationFile>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.9" />
13+
</ItemGroup>
14+
15+
</Project>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace TextVisionComparer;
2+
3+
/// <summary>
4+
/// 图片比较的范围
5+
/// </summary>
6+
/// <param name="Left"></param>
7+
/// <param name="Top"></param>
8+
/// <param name="Right"></param>
9+
/// <param name="Bottom"></param>
10+
public readonly record struct VisionCompareRect(int Left, int Top, int Right, int Bottom)
11+
{
12+
public int Width => Right - Left + 1; // 为什么需要加一?如果有第 0 和第 1 个像素不同,则一共有两个像素不同
13+
public int Height => Bottom - Top + 1;
14+
15+
public int X => Left;
16+
17+
public int Y => Top;
18+
19+
public override string ToString()
20+
{
21+
return $"X={X};Y={Y};W={Width};H={Height}";
22+
}
23+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
namespace TextVisionComparer;
2+
3+
public readonly record struct VisionCompareResult(bool Success, double SimilarityValue, int PixelCount, int DissimilarPixelCount, IReadOnlyList<VisionCompareRect> CompareRectList, string DebugReason)
4+
{
5+
public static VisionCompareResult FailResult(string debugReason) => new VisionCompareResult(false, 0, 0, 0, null!, debugReason);
6+
7+
/// <summary>
8+
/// 是不是相似
9+
/// </summary>
10+
/// <returns></returns>
11+
public bool IsSimilar()
12+
{
13+
if (!Success)
14+
{
15+
// 没有比较的意义
16+
return false;
17+
}
18+
19+
if ((double) DissimilarPixelCount / PixelCount > 0.1)
20+
{
21+
return false;
22+
}
23+
24+
foreach (VisionCompareRect visionCompareRect in CompareRectList)
25+
{
26+
if (visionCompareRect.Width > 5 && visionCompareRect.Height > 5)
27+
{
28+
return false;
29+
}
30+
31+
var area = visionCompareRect.Width * visionCompareRect.Height;
32+
if (area > 20)
33+
{
34+
return false;
35+
}
36+
}
37+
38+
return true;
39+
}
40+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.ComponentModel;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
using SixLabors.ImageSharp;
10+
using SixLabors.ImageSharp.PixelFormats;
11+
12+
namespace TextVisionComparer;
13+
14+
public class VisionComparer
15+
{
16+
/// <summary>
17+
/// 比较两张图片的相似度
18+
/// </summary>
19+
/// <param name="textImageFile1"></param>
20+
/// <param name="textImageFile2"></param>
21+
/// <returns></returns>
22+
public VisionCompareResult Compare(FileInfo textImageFile1, FileInfo textImageFile2)
23+
{
24+
// 对比算法:
25+
// 1. 逐个像素点对比,如果不同则记录下来
26+
// 2. 计算不同的像素点数量
27+
// 3. 计算不同的像素点的范围列表
28+
// 优势:
29+
// 文本很多都是像素边缘差异,通过范围列表可以减少误差
30+
using Image<Rgba32> image1 = Image.Load<Rgba32>(textImageFile1.FullName);
31+
using Image<Rgba32> image2 = Image.Load<Rgba32>(textImageFile2.FullName);
32+
33+
if (image1.Size != image2.Size)
34+
{
35+
return VisionCompareResult.FailResult($"两张图片尺寸不相同,不能进行相似比较");
36+
}
37+
38+
// 总像素点数量
39+
int imageWidth = Math.Min(image1.Width, image2.Width);
40+
int imageHeight = Math.Min(image1.Height, image2.Height);
41+
var pixelCount = imageWidth * imageHeight;
42+
// 不同的像素点数量
43+
int dissimilarPixelCount = 0;
44+
45+
double totalDistanceValue = 0;
46+
47+
double[] pixelDistance = new double[pixelCount];
48+
49+
Parallel.For(0, pixelCount, n =>
50+
{
51+
// ReSharper disable AccessToDisposedClosure
52+
var y = n / imageWidth;
53+
var x = n % imageWidth;
54+
55+
Rgba32 pixel1 = image1[x, y];
56+
Rgba32 pixel2 = image2[x, y];
57+
58+
if (pixel1 == pixel2)
59+
{
60+
pixelDistance[n] = 0;
61+
return;
62+
}
63+
64+
double distance =
65+
(Math.Abs(pixel1.A - pixel2.A))
66+
+ (Math.Abs(pixel1.R - pixel2.R))
67+
+ (Math.Abs(pixel1.G - pixel2.G))
68+
+ (Math.Abs(pixel1.B - pixel2.B));
69+
const int countOfArgb = 4;
70+
// 距离应该计算到 0-1 之间,除以最大值
71+
pixelDistance[n] = distance / (countOfArgb * byte.MaxValue);
72+
});
73+
74+
var mapArray = new BitArray(pixelCount, false);
75+
76+
for (int i = 0; i < pixelDistance.Length; i++)
77+
{
78+
double distance = pixelDistance[i];
79+
totalDistanceValue += distance;
80+
81+
if (distance > 0)
82+
{
83+
dissimilarPixelCount++;
84+
mapArray[i] = true;
85+
}
86+
}
87+
88+
List<VisionCompareRect> list = GetVisionCompareRectList(mapArray, imageWidth, imageHeight);
89+
90+
double similarityValue = 1 - totalDistanceValue / (imageWidth * imageHeight);
91+
return new VisionCompareResult(true, similarityValue, pixelCount, dissimilarPixelCount, list, "成功");
92+
}
93+
94+
private static List<VisionCompareRect> GetVisionCompareRectList(BitArray mapArray, int imageWidth, int imageHeight)
95+
{
96+
var pixelCount = imageWidth * imageHeight;
97+
98+
var list = new List<VisionCompareRect>();
99+
// 尝试寻找有多少范围是存在不相似的
100+
var ignoreArray = new BitArray(pixelCount, false);
101+
for (var i = 0; i < mapArray.Count; i++)
102+
{
103+
if (ignoreArray[i] is true)
104+
{
105+
// 被访问过了
106+
continue;
107+
}
108+
109+
if (mapArray[i] is false)
110+
{
111+
continue;
112+
}
113+
114+
var y = i / imageWidth;
115+
var x = i % imageWidth;
116+
117+
// 向下右扩展
118+
// 处理不了空白范围,或者是溶解效果的差异
119+
var minBottom = GetMaxBottom(x, y);
120+
var maxRight = x;
121+
122+
for (int currentX = x; currentX < imageWidth; currentX++)
123+
{
124+
var index = y * imageWidth + currentX;
125+
if (mapArray[index] is false)
126+
{
127+
break;
128+
}
129+
130+
if (minBottom == y)
131+
{
132+
// 不需要继续找了,只找最小的
133+
}
134+
else
135+
{
136+
var bottom = GetMaxBottom(currentX, y);
137+
minBottom = Math.Min(minBottom, bottom);
138+
}
139+
140+
maxRight = currentX;
141+
}
142+
143+
if (maxRight > x || minBottom > y)
144+
{
145+
VisionCompareRect visionCompareRect = new VisionCompareRect(x, y, maxRight, minBottom);
146+
list.Add(visionCompareRect);
147+
148+
// 添加标记,减少重复访问
149+
for (int currentY = y; currentY <= minBottom; currentY++)
150+
{
151+
for (int currentX = x; currentX <= maxRight; currentX++)
152+
{
153+
var index = currentY * imageWidth + currentX;
154+
ignoreArray[index] = true;
155+
}
156+
}
157+
}
158+
159+
int GetMaxBottom(int initX, int initY)
160+
{
161+
int bottom = initY;
162+
for (int currentY = initY; currentY < imageHeight; currentY++)
163+
{
164+
var index = currentY * imageWidth + initX;
165+
if (mapArray[index] is false)
166+
{
167+
break;
168+
}
169+
170+
bottom = currentY;
171+
}
172+
173+
return bottom;
174+
}
175+
}
176+
177+
return list;
178+
}
179+
}

0 commit comments

Comments
 (0)