Skip to content

Commit 30732d2

Browse files
authored
Merge pull request #128 from Codeuctivity/pixelColorShiftTolerance
Pixel color shift tolerance
2 parents 13c39d6 + a8f273e commit 30732d2

File tree

7 files changed

+138
-36
lines changed

7 files changed

+138
-36
lines changed

.github/FUNDING.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# These are supported funding model platforms
2+
3+
github: [Codeuctivity]
4+
patreon: # Replace with a single Patreon username
5+
open_collective: # Replace with a single Open Collective username
6+
ko_fi: # Replace with a single Ko-fi username
7+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9+
liberapay: # Replace with a single Liberapay username
10+
issuehunt: # Replace with a single IssueHunt username
11+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12+
polar: # Replace with a single Polar username
13+
buy_me_a_coffee: stesee
14+
thanks_dev: # Replace with a single thanks.dev username
15+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

.github/workflows/dotnet.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
with:
2121
dotnet-version: |
2222
8.0.x
23+
9.0.x
2324
- name: Restore dependencies
2425
run: dotnet restore
2526
- name: Build
@@ -38,6 +39,7 @@ jobs:
3839
with:
3940
dotnet-version: |
4041
8.0.x
42+
9.0.x
4143
- name: Restore dependencies
4244
run: dotnet restore
4345
- name: Build
@@ -67,6 +69,7 @@ jobs:
6769
with:
6870
dotnet-version: |
6971
8.0.x
72+
9.0.x
7073
- name: Check formatting
7174
run: dotnet format --verify-no-changes
7275
- name: Restore dependencies

ImageSharpCompare/ImageSharpCompare.cs

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,13 @@ public static bool ImagesHaveEqualSize(Image actualImage, Image expectedImage)
9898
/// <param name="pathImageActual"></param>
9999
/// <param name="pathImageExpected"></param>
100100
/// <param name="resizeOption"></param>
101+
/// <param name="pixelColorShiftTolerance"></param>
101102
/// <returns>True if every pixel of actual is equal to expected</returns>
102-
public static bool ImagesAreEqual(string pathImageActual, string pathImageExpected, ResizeOption resizeOption = ResizeOption.DontResize)
103+
public static bool ImagesAreEqual(string pathImageActual, string pathImageExpected, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0)
103104
{
104105
using var actualImage = Image.Load(pathImageActual);
105106
using var expectedImage = Image.Load(pathImageExpected);
106-
return ImagesAreEqual(actualImage, expectedImage, resizeOption);
107+
return ImagesAreEqual(actualImage, expectedImage, resizeOption, pixelColorShiftTolerance);
107108
}
108109

109110
/// <summary>
@@ -126,8 +127,9 @@ public static bool ImagesAreEqual(Stream actual, Stream expected, ResizeOption r
126127
/// <param name="actual"></param>
127128
/// <param name="expected"></param>
128129
/// <param name="resizeOption"></param>
130+
/// <param name="pixelColorShiftTolerance"></param>
129131
/// <returns>True if every pixel of actual is equal to expected</returns>
130-
public static bool ImagesAreEqual(Image actual, Image expected, ResizeOption resizeOption = ResizeOption.DontResize)
132+
public static bool ImagesAreEqual(Image actual, Image expected, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0)
131133
{
132134
ArgumentNullException.ThrowIfNull(actual);
133135

@@ -142,7 +144,7 @@ public static bool ImagesAreEqual(Image actual, Image expected, ResizeOption res
142144
actualPixelAccessibleImage = ImageSharpPixelTypeConverter.ToRgb24Image(actual, out ownsActual);
143145
expectedPixelAccusableImage = ImageSharpPixelTypeConverter.ToRgb24Image(expected, out ownsExpected);
144146

145-
return ImagesAreEqual(actualPixelAccessibleImage, expectedPixelAccusableImage, resizeOption);
147+
return ImagesAreEqual(actualPixelAccessibleImage, expectedPixelAccusableImage, resizeOption, pixelColorShiftTolerance);
146148
}
147149
finally
148150
{
@@ -163,8 +165,9 @@ public static bool ImagesAreEqual(Image actual, Image expected, ResizeOption res
163165
/// <param name="actual"></param>
164166
/// <param name="expected"></param>
165167
/// <param name="resizeOption"></param>
168+
/// <param name="pixelColorShiftTolerance"></param>
166169
/// <returns>True if every pixel of actual is equal to expected</returns>
167-
public static bool ImagesAreEqual(Image<Rgb24> actual, Image<Rgb24> expected, ResizeOption resizeOption = ResizeOption.DontResize)
170+
public static bool ImagesAreEqual(Image<Rgb24> actual, Image<Rgb24> expected, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0)
168171
{
169172
ArgumentNullException.ThrowIfNull(actual);
170173

@@ -181,10 +184,21 @@ public static bool ImagesAreEqual(Image<Rgb24> actual, Image<Rgb24> expected, Re
181184
{
182185
for (var y = 0; y < actual.Height; y++)
183186
{
184-
if (!actual[x, y].Equals(expected[x, y]))
187+
if (pixelColorShiftTolerance == 0 && !actual[x, y].Equals(expected[x, y]))
185188
{
186189
return false;
187190
}
191+
else if (pixelColorShiftTolerance > 0)
192+
{
193+
var actualPixel = actual[x, y];
194+
var expectedPixel = expected[x, y];
195+
if (Math.Abs(actualPixel.R - expectedPixel.R) > pixelColorShiftTolerance ||
196+
Math.Abs(actualPixel.G - expectedPixel.G) > pixelColorShiftTolerance ||
197+
Math.Abs(actualPixel.B - expectedPixel.B) > pixelColorShiftTolerance)
198+
{
199+
return false;
200+
}
201+
}
188202
}
189203
}
190204

@@ -194,7 +208,7 @@ public static bool ImagesAreEqual(Image<Rgb24> actual, Image<Rgb24> expected, Re
194208
var grown = GrowToSameDimension(actual, expected);
195209
try
196210
{
197-
return ImagesAreEqual(grown.Item1, grown.Item2, ResizeOption.DontResize);
211+
return ImagesAreEqual(grown.Item1, grown.Item2, ResizeOption.DontResize, pixelColorShiftTolerance);
198212
}
199213
finally
200214
{
@@ -350,7 +364,7 @@ public static ICompareResult CalcDiff(Image<Rgb24> actual, Image<Rgb24> expected
350364
var g = Math.Abs(expectedPixel.G - actualPixel.G);
351365
var b = Math.Abs(expectedPixel.B - actualPixel.B);
352366
var sum = r + g + b;
353-
absoluteError += (sum > pixelColorShiftTolerance ? sum : 0);
367+
absoluteError += sum > pixelColorShiftTolerance ? sum : 0;
354368
pixelErrorCount += (sum > pixelColorShiftTolerance) ? 1 : 0;
355369
}
356370
}
@@ -477,7 +491,7 @@ public static ICompareResult CalcDiff(Image<Rgb24> actual, Image<Rgb24> expected
477491
error += b;
478492
}
479493

480-
absoluteError += (error > pixelColorShiftTolerance ? error : 0);
494+
absoluteError += error > pixelColorShiftTolerance ? error : 0;
481495
pixelErrorCount += error > pixelColorShiftTolerance ? 1 : 0;
482496
}
483497
}
@@ -663,14 +677,40 @@ public static Image CalcDiffMaskImage(Image<Rgb24> actual, Image<Rgb24> expected
663677
var actualPixel = actual[x, y];
664678
var expectedPixel = expected[x, y];
665679

666-
var pixel = new Rgb24
680+
if (pixelColorShiftTolerance == 0)
667681
{
668-
R = (byte)Math.Abs(actualPixel.R - expectedPixel.R),
669-
G = (byte)Math.Abs(actualPixel.G - expectedPixel.G),
670-
B = (byte)Math.Abs(actualPixel.B - expectedPixel.B)
671-
};
672-
673-
maskImage[x, y] = pixel;
682+
var pixel = new Rgb24
683+
{
684+
R = (byte)Math.Abs(actualPixel.R - expectedPixel.R),
685+
G = (byte)Math.Abs(actualPixel.G - expectedPixel.G),
686+
B = (byte)Math.Abs(actualPixel.B - expectedPixel.B)
687+
};
688+
689+
maskImage[x, y] = pixel;
690+
}
691+
else
692+
{
693+
var r = Math.Abs(actualPixel.R - expectedPixel.R);
694+
var g = Math.Abs(actualPixel.G - expectedPixel.G);
695+
var b = Math.Abs(actualPixel.B - expectedPixel.B);
696+
697+
var error = r + g + b;
698+
if (error <= pixelColorShiftTolerance)
699+
{
700+
r = 0;
701+
g = 0;
702+
b = 0;
703+
}
704+
705+
var pixel = new Rgb24
706+
{
707+
R = (byte)r,
708+
G = (byte)g,
709+
B = (byte)b
710+
};
711+
712+
maskImage[x, y] = pixel;
713+
}
674714
}
675715
}
676716
return maskImage;

ImageSharpCompare/ImageSharpCompare.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks>net8.0</TargetFrameworks>
3+
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
44
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
55
<GenerateDocumentationFile>true</GenerateDocumentationFile>
66
<RepositoryUrl>https://github.com/Codeuctivity/ImageSharp.Compare</RepositoryUrl>
@@ -45,7 +45,7 @@
4545

4646
<ItemGroup>
4747
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
48-
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.32.0.97167">
48+
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.9.0.115408">
4949
<PrivateAssets>all</PrivateAssets>
5050
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
5151
</PackageReference>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Microsoft Visual Studio Solution File, Format Version 12.00
2+
# Visual Studio Version 17
3+
VisualStudioVersion = 17.5.2.0
4+
MinimumVisualStudioVersion = 10.0.40219.1
5+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharpCompare", "ImageSharpCompare.csproj", "{6D218566-E4FA-E901-D111-08AEB4065B5C}"
6+
EndProject
7+
Global
8+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9+
Debug|Any CPU = Debug|Any CPU
10+
Release|Any CPU = Release|Any CPU
11+
EndGlobalSection
12+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
13+
{6D218566-E4FA-E901-D111-08AEB4065B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
14+
{6D218566-E4FA-E901-D111-08AEB4065B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
15+
{6D218566-E4FA-E901-D111-08AEB4065B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
16+
{6D218566-E4FA-E901-D111-08AEB4065B5C}.Release|Any CPU.Build.0 = Release|Any CPU
17+
EndGlobalSection
18+
GlobalSection(SolutionProperties) = preSolution
19+
HideSolutionNode = FALSE
20+
EndGlobalSection
21+
GlobalSection(ExtensibilityGlobals) = postSolution
22+
SolutionGuid = {239FA1C2-CD5C-4D95-BE3C-97B7B1BF4F3C}
23+
EndGlobalSection
24+
EndGlobal

ImageSharpCompareTestNunit/ImageSharpCompareTest.cs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,18 @@ public void ShouldVerifyThatImagesWithDifferentSizeAreEqual(string pathActual, s
9393

9494
Assert.That(ImageSharpCompare.ImagesAreEqual(absolutePathActual, absolutePathExpected, resizeOption), Is.EqualTo(expectedResult));
9595
}
96+
[Test]
97+
[TestCase(colorShift1, colorShift2, ResizeOption.DontResize, 0, false)]
98+
[TestCase(colorShift1, colorShift2, ResizeOption.Resize, 0, false)]
99+
[TestCase(colorShift1, colorShift2, ResizeOption.DontResize, 15, true)]
100+
[TestCase(colorShift1, colorShift2, ResizeOption.Resize, 15, true)]
101+
public void ShouldVerifyThatImagesWithColorShift(string pathActual, string pathExpected, ResizeOption resizeOption, int expectedColorShift, bool expectedResult)
102+
{
103+
var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
104+
var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
105+
106+
Assert.That(ImageSharpCompare.ImagesAreEqual(absolutePathActual, absolutePathExpected, resizeOption, expectedColorShift), Is.EqualTo(expectedResult));
107+
}
96108

97109
[Test]
98110
[TestCase(jpg0Rgb24, jpg0Rgb24)]
@@ -157,7 +169,6 @@ public void ShouldVerifyThatImageSharpImagesAreEqualBgra5551(string pathActual,
157169
}
158170

159171
[Test]
160-
[TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, null, 0)]
161172
[TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, ResizeOption.DontResize, 0)]
162173
[TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, ResizeOption.Resize, 0)]
163174
[TestCase(jpg1Rgb24, png1Rgba32, 382669, 2.3673566603152607d, 140893, 87.162530004206772d, ResizeOption.DontResize, 0)]
@@ -204,7 +215,6 @@ public void ShouldVerifyThatCalcDiffThrowsOnDifferentImageSizes(string pathPic1,
204215
}
205216

206217
[Test]
207-
[TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, null)]
208218
[TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, ResizeOption.DontResize)]
209219
[TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, ResizeOption.Resize)]
210220
[TestCase(jpg1Rgb24, png1Rgba32, 382669, 2.3673566603152607d, 140893, 87.162530004206772d, ResizeOption.DontResize)]
@@ -230,26 +240,32 @@ public void ShouldVerifyThatImageStreamsAreSemiEqual(string pathPic1, string pat
230240
Assert.That(diff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage");
231241
}
232242

233-
[TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, null)]
234-
[TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize)]
235-
[TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.Resize)]
236-
[TestCase(pngWhite2x2px, pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize)]
237-
[TestCase(pngBlack4x4px, pngWhite2x2px, 0, 0, 0, 0, ResizeOption.Resize)]
238-
[TestCase(renderedForm1, renderedForm2, 0, 0, 0, 0, ResizeOption.Resize)]
239-
[TestCase(renderedForm2, renderedForm1, 0, 0, 0, 0, ResizeOption.Resize)]
240-
public void CalcDiffMaskImage(string pathPic1, string pathPic2, double expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption)
243+
[TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize, 0, false)]
244+
[TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.Resize, 0, false)]
245+
[TestCase(pngWhite2x2px, pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize, 0, false)]
246+
[TestCase(pngBlack4x4px, pngWhite2x2px, 0, 0, 0, 0, ResizeOption.Resize, 0, false)]
247+
[TestCase(renderedForm1, renderedForm2, 0, 0, 0, 0, ResizeOption.Resize, 0, false)]
248+
[TestCase(renderedForm2, renderedForm1, 0, 0, 0, 0, ResizeOption.Resize, 0, false)]
249+
[TestCase(colorShift1, colorShift1, 0, 0, 0, 0, ResizeOption.DontResize, 15, true)]
250+
[TestCase(colorShift1, colorShift1, 0, 0, 0, 0, ResizeOption.Resize, 15, true)]
251+
[TestCase(colorShift1, colorShift2, 0, 0, 0, 0, ResizeOption.Resize, 15, true)]
252+
[TestCase(colorShift1, colorShift2, 0, 0, 0, 0, ResizeOption.DontResize, 15, true)]
253+
[TestCase(colorShift1, colorShift2, 0, 0, 0, 0, ResizeOption.Resize, 14, false)]
254+
[TestCase(colorShift1, colorShift2, 0, 0, 0, 0, ResizeOption.DontResize, 14, false)]
255+
public void CalcDiffMaskImage(string pathPic1, string pathPic2, double expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption, int expectedColorShift, bool expectMaskToBeBlack)
241256
{
242257
var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
243258
var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
244259
var differenceMask = Path.GetTempFileName() + "differenceMask.png";
245260

246261
using (var fileStreamDifferenceMask = File.Create(differenceMask))
247-
using (var maskImage = ImageSharpCompare.CalcDiffMaskImage(absolutePathPic1, absolutePathPic2, resizeOption))
262+
using (var maskImage = ImageSharpCompare.CalcDiffMaskImage(absolutePathPic1, absolutePathPic2, resizeOption, expectedColorShift))
248263
{
249264
ImageExtensions.SaveAsPng(maskImage, fileStreamDifferenceMask);
265+
Assert.That(IsImageEntirelyBlack(maskImage), Is.EqualTo(expectMaskToBeBlack));
250266
}
251267

252-
var maskedDiff = ImageSharpCompare.CalcDiff(absolutePathPic1, absolutePathPic2, differenceMask, resizeOption);
268+
var maskedDiff = ImageSharpCompare.CalcDiff(absolutePathPic1, absolutePathPic2, differenceMask, resizeOption, expectedColorShift);
253269
File.Delete(differenceMask);
254270

255271
Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError");

ImageSharpCompareTestNunit/ImageSharpCompareTestNunit.csproj

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>net8.0</TargetFrameworks>
4+
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
55
<IsPackable>false</IsPackable>
66
<Nullable>enable</Nullable>
77
<EnableNETAnalyzers>true</EnableNETAnalyzers>
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="coverlet.collector" Version="6.0.2">
11+
<PackageReference Include="coverlet.collector" Version="6.0.4">
1212
<PrivateAssets>all</PrivateAssets>
1313
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1414
</PackageReference>
15-
<PackageReference Include="nunit" Version="4.2.2" />
16-
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.32.0.97167">
15+
<PackageReference Include="nunit" Version="4.3.2" />
16+
<PackageReference Include="NUnit.Analyzers" Version="4.7.0">
17+
<PrivateAssets>all</PrivateAssets>
18+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
19+
</PackageReference>
20+
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.9.0.115408">
1721
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1822
<PrivateAssets>all</PrivateAssets>
1923
</PackageReference>
20-
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0">
24+
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0">
2125
<PrivateAssets>all</PrivateAssets>
2226
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2327
</PackageReference>
24-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
28+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
2529
</ItemGroup>
2630

2731
<ItemGroup>

0 commit comments

Comments
 (0)