Skip to content

Commit 909edd7

Browse files
authored
[Visual Testing] Image comparison functions (#88) +semver: feature
* prepare ImageExtensions and ScreenshotExtensions to be used for visualization feature * add ImageComparator and VisualizationConfiguration
1 parent 55cf0e9 commit 909edd7

File tree

12 files changed

+502
-2
lines changed

12 files changed

+502
-2
lines changed

Aquality.Selenium.Core/src/Aquality.Selenium.Core/Applications/Startup.cs

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Aquality.Selenium.Core.Localization;
55
using Aquality.Selenium.Core.Logging;
66
using Aquality.Selenium.Core.Utilities;
7+
using Aquality.Selenium.Core.Visualization;
78
using Aquality.Selenium.Core.Waitings;
89
using Microsoft.Extensions.DependencyInjection;
910
using System;
@@ -36,6 +37,8 @@ public virtual IServiceCollection ConfigureServices(IServiceCollection services,
3637
services.AddSingleton<ILoggerConfiguration, LoggerConfiguration>();
3738
services.AddSingleton<ITimeoutConfiguration, TimeoutConfiguration>();
3839
services.AddSingleton<IRetryConfiguration, RetryConfiguration>();
40+
services.AddSingleton<IVisualizationConfiguration, VisualizationConfiguration>();
41+
services.AddSingleton<IImageComparator, ImageComparator>();
3942
services.AddSingleton<ILocalizationManager, LocalizationManager>();
4043
services.AddSingleton<ILocalizedLogger, LocalizedLogger>();
4144
services.AddSingleton<IActionRetrier, ActionRetrier>();

Aquality.Selenium.Core/src/Aquality.Selenium.Core/Aquality.Selenium.Core.csproj

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
4848
<PackageReference Include="NLog" Version="4.7.10" />
4949
<PackageReference Include="Selenium.Support" Version="3.141.0" />
50-
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
50+
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
51+
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
5152
</ItemGroup>
5253
</Project>

Aquality.Selenium.Core/src/Aquality.Selenium.Core/Aquality.Selenium.Core.xml

+127
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace Aquality.Selenium.Core.Configurations
2+
{
3+
/// <summary>
4+
/// Represents visualization configuration, used for image comparison.
5+
/// </summary>
6+
public interface IVisualizationConfiguration
7+
{
8+
/// <summary>
9+
/// Default threshold used for image comparison.
10+
/// </summary>
11+
float DefaultThreshold { get; }
12+
13+
/// <summary>
14+
/// Width of the image resized for comparison.
15+
/// </summary>
16+
int ComparisonWidth { get; }
17+
18+
/// <summary>
19+
/// Height of the image resized for comparison.
20+
/// </summary>
21+
int ComparisonHeight { get; }
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Aquality.Selenium.Core.Utilities;
2+
3+
namespace Aquality.Selenium.Core.Configurations
4+
{
5+
/// <summary>
6+
/// Represents visualization configuration, used for image comparison.
7+
/// Uses <see cref="ISettingsFile"/> as source for configuration values.
8+
/// </summary>
9+
public class VisualizationConfiguration : IVisualizationConfiguration
10+
{
11+
private readonly ISettingsFile settingsFile;
12+
13+
/// <summary>
14+
/// Instantiates class using <see cref="ISettingsFile"/> with visualization settings.
15+
/// </summary>
16+
/// <param name="settingsFile">Settings file.</param>
17+
public VisualizationConfiguration(ISettingsFile settingsFile)
18+
{
19+
this.settingsFile = settingsFile;
20+
}
21+
22+
public float DefaultThreshold => settingsFile.GetValueOrDefault(".visualization.defaultThreshold", 0.012f);
23+
24+
public int ComparisonWidth => settingsFile.GetValueOrDefault(".visualization.comparisonWidth", 16);
25+
26+
public int ComparisonHeight => settingsFile.GetValueOrDefault(".visualization.comparisonHeight", 16);
27+
}
28+
}

Aquality.Selenium.Core/src/Aquality.Selenium.Core/Resources/settings.json

+5
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,10 @@
1515
},
1616
"elementCache": {
1717
"isEnabled": false
18+
},
19+
"visualization": {
20+
"defaultThreshold": 0.012,
21+
"comparisonWidth": 16,
22+
"comparisonHeight": 16
1823
}
1924
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Aquality.Selenium.Core.Configurations;
2+
using System.Drawing;
3+
4+
namespace Aquality.Selenium.Core.Visualization
5+
{
6+
/// <summary>
7+
/// Compares images with defined threshold.
8+
/// Uses resizing and gray-scaling to simplify comparison.
9+
/// Default implementation uses parameters from <see cref="IVisualizationConfiguration"/>.
10+
/// </summary>
11+
public interface IImageComparator
12+
{
13+
/// <summary>
14+
/// Gets the difference between two images as a percentage
15+
/// </summary>
16+
/// <param name="thisOne">The first image</param>
17+
/// <param name="theOtherOne">The image to compare with</param>
18+
/// <param name="threshold">How big a difference will be ignored as a percentage - value between 0 and 1. </param>
19+
/// <returns>The difference between the two images as a percentage - value between 0 and 1.</returns>
20+
float PercentageDifference(Image thisOne, Image theOtherOne, float? threshold = null);
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using Aquality.Selenium.Core.Configurations;
2+
using System;
3+
using System.Drawing;
4+
5+
namespace Aquality.Selenium.Core.Visualization
6+
{
7+
/// <summary>
8+
/// Compares images with defined threshold.
9+
/// Uses resizing and gray-scaling to simplify comparison.
10+
/// Uses parameters from <see cref="IVisualizationConfiguration"/>.
11+
/// Special thanks to https://www.codeproject.com/Articles/374386/Simple-image-comparison-in-NET
12+
/// </summary>
13+
public class ImageComparator : IImageComparator
14+
{
15+
private const int ThresholdDivisor = 255;
16+
private readonly IVisualizationConfiguration visualizationConfiguration;
17+
18+
public ImageComparator(IVisualizationConfiguration visualizationConfiguration)
19+
{
20+
this.visualizationConfiguration = visualizationConfiguration;
21+
}
22+
23+
private float DefaultThreshold => visualizationConfiguration.DefaultThreshold;
24+
private int ComparisonHeight => visualizationConfiguration.ComparisonHeight;
25+
private int ComparisonWidth => visualizationConfiguration.ComparisonWidth;
26+
27+
/// <summary>
28+
/// Gets the difference between two images as a percentage
29+
/// </summary>
30+
/// <param name="thisOne">The first image</param>
31+
/// <param name="theOtherOne">The image to compare with</param>
32+
/// <param name="threshold">How big a difference will be ignored as a percentage - value between 0 and 1.
33+
/// If the value is null, the default value is got from <see cref="IVisualizationConfiguration"/>.</param>
34+
/// <returns>The difference between the two images as a percentage - value between 0 and 1.</returns>
35+
/// <remarks>See https://web.archive.org/web/20130208001434/http://tech.pro:80/tutorial/660/csharp-tutorial-convert-a-color-image-to-grayscale for more details.</remarks>
36+
public virtual float PercentageDifference(Image thisOne, Image theOtherOne, float? threshold = null)
37+
{
38+
var thresholdValue = threshold ?? DefaultThreshold;
39+
if (thresholdValue < 0 || thresholdValue > 1)
40+
{
41+
throw new ArgumentOutOfRangeException(nameof(threshold), thresholdValue, "Threshold should be between 0 and 1");
42+
}
43+
44+
var byteThreshold = Convert.ToByte(thresholdValue * ThresholdDivisor);
45+
return PercentageDifference(thisOne, theOtherOne, byteThreshold);
46+
}
47+
48+
/// <summary>
49+
/// Gets the difference between two images as a percentage
50+
/// </summary>
51+
/// <param name="thisOne">The first image</param>
52+
/// <param name="theOtherOne">The image to compare with</param>
53+
/// <param name="threshold">How big a difference (out of 255) will be ignored - the default is 3.</param>
54+
/// <returns>The difference between the two images as a percentage</returns>
55+
/// <remarks>See https://web.archive.org/web/20130208001434/http://tech.pro:80/tutorial/660/csharp-tutorial-convert-a-color-image-to-grayscale for more details</remarks>
56+
protected virtual float PercentageDifference(Image thisOne, Image theOtherOne, byte threshold = 3)
57+
{
58+
var differences = GetDifferences(thisOne, theOtherOne);
59+
60+
int diffPixels = 0;
61+
62+
foreach (byte b in differences)
63+
{
64+
if (b > threshold) { diffPixels++; }
65+
}
66+
67+
return diffPixels / ((float)ComparisonWidth * ComparisonHeight);
68+
}
69+
70+
/// <summary>
71+
/// Finds the differences between two images and returns them in a double-array
72+
/// </summary>
73+
/// <param name="thisOne">The first image</param>
74+
/// <param name="theOtherOne">The image to compare with</param>
75+
/// <returns>the differences between the two images as a double-array</returns>
76+
protected virtual byte[,] GetDifferences(Image thisOne, Image theOtherOne)
77+
{
78+
var firstGray = GetResizedGrayScaleValues(thisOne);
79+
var secondGray = GetResizedGrayScaleValues(theOtherOne);
80+
81+
var differences = new byte[ComparisonWidth, ComparisonHeight];
82+
for (int y = 0; y < ComparisonHeight; y++)
83+
{
84+
for (int x = 0; x < ComparisonWidth; x++)
85+
{
86+
differences[x, y] = (byte)Math.Abs(firstGray[x, y] - secondGray[x, y]);
87+
}
88+
}
89+
90+
return differences;
91+
}
92+
93+
/// <summary>
94+
/// Gets the lightness of the image in sections (by default 256 sections, 16x16)
95+
/// </summary>
96+
/// <param name="img">The image to get the lightness for</param>
97+
/// <returns>A double-array (16x16 by default) containing the lightness of the sections(256 by default)</returns>
98+
protected virtual byte[,] GetResizedGrayScaleValues(Image img)
99+
{
100+
using (var thisOne = (Bitmap)img.Resize(ComparisonWidth, ComparisonHeight).GetGrayScaleVersion())
101+
{
102+
byte[,] grayScale = new byte[thisOne.Width, thisOne.Height];
103+
104+
for (int y = 0; y < thisOne.Height; y++)
105+
{
106+
for (int x = 0; x < thisOne.Width; x++)
107+
{
108+
grayScale[x, y] = (byte)Math.Abs(thisOne.GetPixel(x, y).R);
109+
}
110+
}
111+
112+
return grayScale;
113+
}
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)