Update: image tests
This commit is contained in:
Родитель
63beab82d8
Коммит
974b717ae1
|
@ -4,16 +4,8 @@ root = true
|
|||
[*.cs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
csharp_style_var_for_built_in_types = false:warning
|
||||
csharp_style_var_elsewhere = false:warning
|
||||
csharp_style_var_when_type_is_apparent = true:warning
|
||||
end_of_line = crlf
|
||||
dotnet_sort_system_directives_first = true
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
|
||||
dotnet_style_predefined_type_for_member_access = true:warning
|
||||
dotnet_style_qualification_for_field = true:warning
|
||||
dotnet_style_qualification_for_method = true:warning
|
||||
dotnet_style_qualification_for_property = true:warning
|
||||
|
||||
[*.tt]
|
||||
indent_style = space
|
||||
|
|
|
@ -45,4 +45,10 @@
|
|||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<LangVersion>Latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<LangVersion>Latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
|
@ -63,8 +63,8 @@ namespace EquinoxLabs.SVGSharpie.ImageSharp
|
|||
|
||||
private static Image<TPixel> RenderInner<TPixel>(SvgDocument document, int? targetWidth, int? targetHeight) where TPixel : struct, IPixel<TPixel>
|
||||
{
|
||||
float? width = targetWidth ?? document.RootElement.Width ?? document.RootElement.ViewWidth;
|
||||
float? height = targetHeight ?? document.RootElement.Height ?? document.RootElement.ViewWidth;
|
||||
float? width = targetWidth ?? document.RootElement.ViewWidth;
|
||||
float? height = targetHeight ?? document.RootElement.ViewWidth;
|
||||
|
||||
if (!width.HasValue || !height.HasValue)
|
||||
{
|
||||
|
|
|
@ -41,4 +41,10 @@
|
|||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<LangVersion>Latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<LangVersion>Latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
|
@ -7,6 +7,12 @@
|
|||
<AssemblyName>EQL.SVGSharpie.ImageSharp.Tests</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<LangVersion>Latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<LangVersion>Latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.0" />
|
||||
|
@ -17,4 +23,10 @@
|
|||
<ProjectReference Include="..\..\src\EquinoxLabs.SVGSharpie.ImageSharp\EquinoxLabs.SVGSharpie.ImageSharp.csproj" />
|
||||
<ProjectReference Include="..\..\src\EquinoxLabs.SVGSharpie\EquinoxLabs.SVGSharpie.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Example\Scratch\tiger.html" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="ImageComparison\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\src\SixLabors.Svg\SixLabors.ImageSharp.Svg.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="source.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="source.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="tiger.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="tiger.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<input type="range" onchange="document.getElementById('svg').style.opacity = this.value;" value="0.5" min="0" max="1" step="0.01" />
|
||||
<div style="position:relative">
|
||||
<div style="position:absolute; left:0; top :0px; background-color:white" id="png">
|
||||
<img src="tiger.png" />
|
||||
</div>
|
||||
<div style="position:absolute; left:0; top :0px; background-color:white; opacity:0.5" id="svg">
|
||||
<img src="tiger.svg" />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SixLabors.ImageSharp.Advanced;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
using SixLabors.Primitives;
|
||||
|
||||
namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison
|
||||
{
|
||||
public class ExactImageComparer : ImageComparer
|
||||
{
|
||||
public static ExactImageComparer Instance { get; } = new ExactImageComparer();
|
||||
|
||||
public override ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>(
|
||||
ImageFrame<TPixelA> expected,
|
||||
ImageFrame<TPixelB> actual)
|
||||
{
|
||||
if (expected.Size() != actual.Size())
|
||||
{
|
||||
throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!");
|
||||
}
|
||||
|
||||
int width = actual.Width;
|
||||
|
||||
// TODO: Comparing through Rgba64 may not be robust enough because of the existance of super high precision pixel types.
|
||||
|
||||
var aBuffer = new Rgba32[width];
|
||||
var bBuffer = new Rgba32[width];
|
||||
|
||||
var differences = new List<PixelDifference>();
|
||||
|
||||
for (int y = 0; y < actual.Height; y++)
|
||||
{
|
||||
Span<TPixelA> aSpan = expected.GetPixelRowSpan(y);
|
||||
Span<TPixelB> bSpan = actual.GetPixelRowSpan(y);
|
||||
for(var i = 0; i<aSpan.Length; i++) { aSpan[i].ToRgba32(ref aBuffer[i]); }
|
||||
for(var i = 0; i<bSpan.Length; i++) { bSpan[i].ToRgba32(ref bBuffer[i]); }
|
||||
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
Rgba32 aPixel = aBuffer[x];
|
||||
Rgba32 bPixel = bBuffer[x];
|
||||
|
||||
if (aPixel != bPixel)
|
||||
{
|
||||
var diff = new PixelDifference(new Point(x, y), aPixel, bPixel);
|
||||
differences.Add(diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ImageSimilarityReport<TPixelA, TPixelB>(expected, actual, differences);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison
|
||||
{
|
||||
public class ImageDifferenceIsOverThresholdException : ImagesSimilarityException
|
||||
{
|
||||
public ImageSimilarityReport[] Reports { get; }
|
||||
|
||||
public ImageDifferenceIsOverThresholdException(IEnumerable<ImageSimilarityReport> reports)
|
||||
: base("Image difference is over threshold!" + StringifyReports(reports))
|
||||
{
|
||||
this.Reports = reports.ToArray();
|
||||
}
|
||||
|
||||
private static string StringifyReports(IEnumerable<ImageSimilarityReport> reports)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append(Environment.NewLine);
|
||||
|
||||
int i = 0;
|
||||
foreach (ImageSimilarityReport r in reports)
|
||||
{
|
||||
sb.Append($"Report ImageFrame {i}: ");
|
||||
sb.Append(r);
|
||||
sb.Append(Environment.NewLine);
|
||||
i++;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) Six Labors and contributors.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
using SixLabors.Primitives;
|
||||
|
||||
namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison
|
||||
{
|
||||
public class ImageDimensionsMismatchException : ImagesSimilarityException
|
||||
{
|
||||
public ImageDimensionsMismatchException(Size expectedSize, Size actualSize)
|
||||
: base((string)$"The image dimensions {actualSize} do not match the expected {expectedSize}!")
|
||||
{
|
||||
this.ExpectedSize = expectedSize;
|
||||
this.ActualSize = actualSize;
|
||||
}
|
||||
|
||||
public Size ExpectedSize { get; }
|
||||
public Size ActualSize { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison
|
||||
{
|
||||
using System;
|
||||
|
||||
public class ImagesSimilarityException : Exception
|
||||
{
|
||||
public ImagesSimilarityException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
// Copyright (c) Six Labors and contributors.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
using SixLabors.Primitives;
|
||||
|
||||
namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison
|
||||
{
|
||||
public abstract class ImageComparer
|
||||
{
|
||||
public static ImageComparer Exact { get; } = Tolerant(0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Returns an instance of <see cref="TolerantImageComparer"/>.
|
||||
/// Individual manhattan pixel difference is only added to total image difference when the individual difference is over 'perPixelManhattanThreshold'.
|
||||
/// </summary>
|
||||
public static ImageComparer Tolerant(
|
||||
float imageThreshold = TolerantImageComparer.DefaultImageThreshold,
|
||||
int perPixelManhattanThreshold = 0)
|
||||
{
|
||||
return new TolerantImageComparer(imageThreshold, perPixelManhattanThreshold);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Tolerant(imageThresholdInPercents/100)
|
||||
/// </summary>
|
||||
public static ImageComparer TolerantPercentage(float imageThresholdInPercents, int perPixelManhattanThreshold = 0)
|
||||
=> Tolerant(imageThresholdInPercents / 100F, perPixelManhattanThreshold);
|
||||
|
||||
public abstract ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>(
|
||||
ImageFrame<TPixelA> expected,
|
||||
ImageFrame<TPixelB> actual)
|
||||
where TPixelA : struct, IPixel<TPixelA> where TPixelB : struct, IPixel<TPixelB>;
|
||||
}
|
||||
|
||||
public static class ImageComparerExtensions
|
||||
{
|
||||
public static ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>(
|
||||
this ImageComparer comparer,
|
||||
Image<TPixelA> expected,
|
||||
Image<TPixelB> actual)
|
||||
where TPixelA : struct, IPixel<TPixelA> where TPixelB : struct, IPixel<TPixelB>
|
||||
{
|
||||
return comparer.CompareImagesOrFrames(expected.Frames.RootFrame, actual.Frames.RootFrame);
|
||||
}
|
||||
|
||||
public static IEnumerable<ImageSimilarityReport<TPixelA, TPixelB>> CompareImages<TPixelA, TPixelB>(
|
||||
this ImageComparer comparer,
|
||||
Image<TPixelA> expected,
|
||||
Image<TPixelB> actual)
|
||||
where TPixelA : struct, IPixel<TPixelA> where TPixelB : struct, IPixel<TPixelB>
|
||||
{
|
||||
var result = new List<ImageSimilarityReport<TPixelA, TPixelB>>();
|
||||
|
||||
if (expected.Frames.Count != actual.Frames.Count)
|
||||
{
|
||||
throw new Exception("Frame count does not match!");
|
||||
}
|
||||
for (int i = 0; i < expected.Frames.Count; i++)
|
||||
{
|
||||
ImageSimilarityReport<TPixelA, TPixelB> report = comparer.CompareImagesOrFrames(expected.Frames[i], actual.Frames[i]);
|
||||
if (!report.IsEmpty)
|
||||
{
|
||||
result.Add(report);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void VerifySimilarity<TPixelA, TPixelB>(
|
||||
this ImageComparer comparer,
|
||||
Image<TPixelA> expected,
|
||||
Image<TPixelB> actual)
|
||||
where TPixelA : struct, IPixel<TPixelA> where TPixelB : struct, IPixel<TPixelB>
|
||||
{
|
||||
if (expected.Size() != actual.Size())
|
||||
{
|
||||
throw new ImageDimensionsMismatchException(expected.Size(), actual.Size());
|
||||
}
|
||||
|
||||
if (expected.Frames.Count != actual.Frames.Count)
|
||||
{
|
||||
throw new ImagesSimilarityException("Image frame count does not match!");
|
||||
}
|
||||
|
||||
IEnumerable<ImageSimilarityReport> reports = comparer.CompareImages(expected, actual);
|
||||
if (reports.Any())
|
||||
{
|
||||
throw new ImageDifferenceIsOverThresholdException(reports);
|
||||
}
|
||||
}
|
||||
|
||||
public static void VerifySimilarityIgnoreRegion<TPixelA, TPixelB>(
|
||||
this ImageComparer comparer,
|
||||
Image<TPixelA> expected,
|
||||
Image<TPixelB> actual,
|
||||
Rectangle ignoredRegion)
|
||||
where TPixelA : struct, IPixel<TPixelA>
|
||||
where TPixelB : struct, IPixel<TPixelB>
|
||||
{
|
||||
if (expected.Size() != actual.Size())
|
||||
{
|
||||
throw new ImageDimensionsMismatchException(expected.Size(), actual.Size());
|
||||
}
|
||||
|
||||
if (expected.Frames.Count != actual.Frames.Count)
|
||||
{
|
||||
throw new ImagesSimilarityException("Image frame count does not match!");
|
||||
}
|
||||
|
||||
IEnumerable<ImageSimilarityReport<TPixelA, TPixelB>> reports = comparer.CompareImages(expected, actual);
|
||||
if (reports.Any())
|
||||
{
|
||||
var cleanedReports = new List<ImageSimilarityReport<TPixelA, TPixelB>>(reports.Count());
|
||||
foreach (ImageSimilarityReport<TPixelA, TPixelB> r in reports)
|
||||
{
|
||||
IEnumerable<PixelDifference> outsideChanges = r.Differences.Where(
|
||||
x =>
|
||||
!(ignoredRegion.X <= x.Position.X
|
||||
&& x.Position.X <= ignoredRegion.Right
|
||||
&& ignoredRegion.Y <= x.Position.Y
|
||||
&& x.Position.Y <= ignoredRegion.Bottom));
|
||||
|
||||
if (outsideChanges.Any())
|
||||
{
|
||||
cleanedReports.Add(new ImageSimilarityReport<TPixelA, TPixelB>(r.ExpectedImage, r.ActualImage, outsideChanges, null));
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedReports.Count > 0)
|
||||
{
|
||||
throw new ImageDifferenceIsOverThresholdException(cleanedReports);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison
|
||||
{
|
||||
public class ImageSimilarityReport
|
||||
{
|
||||
protected ImageSimilarityReport(
|
||||
object expectedImage,
|
||||
object actualImage,
|
||||
IEnumerable<PixelDifference> differences,
|
||||
float? totalNormalizedDifference = null)
|
||||
{
|
||||
this.ExpectedImage = expectedImage;
|
||||
this.ActualImage = actualImage;
|
||||
this.TotalNormalizedDifference = totalNormalizedDifference;
|
||||
this.Differences = differences.ToArray();
|
||||
}
|
||||
|
||||
public object ExpectedImage { get; }
|
||||
|
||||
public object ActualImage { get; }
|
||||
|
||||
// TODO: This should not be a nullable value!
|
||||
public float? TotalNormalizedDifference { get; }
|
||||
|
||||
public string DifferencePercentageString
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!this.TotalNormalizedDifference.HasValue)
|
||||
{
|
||||
return "?";
|
||||
}
|
||||
else if (this.TotalNormalizedDifference == 0)
|
||||
{
|
||||
return "0%";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{this.TotalNormalizedDifference.Value * 100:0.0000}%";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PixelDifference[] Differences { get; }
|
||||
|
||||
public bool IsEmpty => this.Differences.Length == 0;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return this.IsEmpty ? "[SimilarImages]" : this.PrintDifference();
|
||||
}
|
||||
|
||||
private string PrintDifference()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
if (this.TotalNormalizedDifference.HasValue)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"Total difference: {this.DifferencePercentageString}");
|
||||
}
|
||||
int max = Math.Min(5, this.Differences.Length);
|
||||
|
||||
for (int i = 0; i < max; i++)
|
||||
{
|
||||
sb.Append(this.Differences[i]);
|
||||
if (i < max - 1)
|
||||
{
|
||||
sb.AppendFormat(";{0}", Environment.NewLine);
|
||||
}
|
||||
}
|
||||
if (this.Differences.Length >= 5)
|
||||
{
|
||||
sb.Append("...");
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class ImageSimilarityReport<TPixelA, TPixelB> : ImageSimilarityReport
|
||||
where TPixelA : struct, IPixel<TPixelA>
|
||||
where TPixelB : struct, IPixel<TPixelB>
|
||||
{
|
||||
public ImageSimilarityReport(
|
||||
ImageFrame<TPixelA> expectedImage,
|
||||
ImageFrame<TPixelB> actualImage,
|
||||
IEnumerable<PixelDifference> differences,
|
||||
float? totalNormalizedDifference = null)
|
||||
: base(expectedImage, actualImage, differences, totalNormalizedDifference)
|
||||
{
|
||||
}
|
||||
|
||||
public static ImageSimilarityReport<TPixelA, TPixelB> Empty =>
|
||||
new ImageSimilarityReport<TPixelA, TPixelB>(null, null, Enumerable.Empty<PixelDifference>(), 0f);
|
||||
|
||||
public new ImageFrame<TPixelA> ExpectedImage => (ImageFrame<TPixelA>)base.ExpectedImage;
|
||||
|
||||
public new ImageFrame<TPixelB> ActualImage => (ImageFrame<TPixelB>)base.ActualImage;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.Primitives;
|
||||
|
||||
namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison
|
||||
{
|
||||
public readonly struct PixelDifference
|
||||
{
|
||||
public PixelDifference(
|
||||
Point position,
|
||||
int redDifference,
|
||||
int greenDifference,
|
||||
int blueDifference,
|
||||
int alphaDifference)
|
||||
{
|
||||
this.Position = position;
|
||||
this.RedDifference = redDifference;
|
||||
this.GreenDifference = greenDifference;
|
||||
this.BlueDifference = blueDifference;
|
||||
this.AlphaDifference = alphaDifference;
|
||||
}
|
||||
|
||||
public PixelDifference(Point position, Rgba32 expected, Rgba32 actual)
|
||||
: this(position,
|
||||
actual.R - expected.R,
|
||||
actual.G - expected.G,
|
||||
actual.B - expected.B,
|
||||
actual.A - expected.A)
|
||||
{
|
||||
}
|
||||
|
||||
public Point Position { get; }
|
||||
|
||||
public int RedDifference { get; }
|
||||
public int GreenDifference { get; }
|
||||
public int BlueDifference { get; }
|
||||
public int AlphaDifference { get; }
|
||||
|
||||
public override string ToString() => $"[Δ({this.RedDifference},{this.GreenDifference},{this.BlueDifference},{this.AlphaDifference}) @ ({this.Position.X},{this.Position.Y})]";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using SixLabors.ImageSharp.Advanced;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
using SixLabors.Primitives;
|
||||
|
||||
namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison
|
||||
{
|
||||
public class TolerantImageComparer : ImageComparer
|
||||
{
|
||||
// 1% of all pixels in a 100*100 pixel area are allowed to have a difference of 1 unit
|
||||
// 257 = (1 / 255) * 65535.
|
||||
public const float DefaultImageThreshold = 257F / (100 * 100 * 65535);
|
||||
|
||||
/// <summary>
|
||||
/// Individual manhattan pixel difference is only added to total image difference when the individual difference is over 'perPixelManhattanThreshold'.
|
||||
/// </summary>
|
||||
/// <param name="imageThreshold">The maximal tolerated difference represented by a value between 0.0 and 1.0 scaled to 0 and 65535.</param>
|
||||
/// <param name="perPixelManhattanThreshold">Gets the threshold of the individual pixels before they acumulate towards the overall difference.</param>
|
||||
public TolerantImageComparer(float imageThreshold, int perPixelManhattanThreshold = 0)
|
||||
{
|
||||
this.ImageThreshold = imageThreshold;
|
||||
this.PerPixelManhattanThreshold = perPixelManhattanThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Gets the maximal tolerated difference represented by a value between 0.0 and 1.0 scaled to 0 and 65535.
|
||||
/// Examples of percentage differences on a single pixel:
|
||||
/// 1. PixelA = (65535,65535,65535,0) PixelB =(0,0,0,65535) leads to 100% difference on a single pixel
|
||||
/// 2. PixelA = (65535,65535,65535,0) PixelB =(65535,65535,65535,65535) leads to 25% difference on a single pixel
|
||||
/// 3. PixelA = (65535,65535,65535,0) PixelB =(32767,32767,32767,32767) leads to 50% difference on a single pixel
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The total differences is the sum of all pixel differences normalized by image dimensions!
|
||||
/// The individual distances are calculated using the Manhattan function:
|
||||
/// <see>
|
||||
/// <cref>https://en.wikipedia.org/wiki/Taxicab_geometry</cref>
|
||||
/// </see>
|
||||
/// ImageThresholdInPercents = 1/255 = 257/65535 means that we allow one unit difference per channel on a 1x1 image
|
||||
/// ImageThresholdInPercents = 1/(100*100*255) = 257/(100*100*65535) means that we allow only one unit difference per channel on a 100x100 image
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public float ImageThreshold { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the threshold of the individual pixels before they acumulate towards the overall difference.
|
||||
/// For an individual <see cref="Rgba64"/> pixel pair the value is the Manhattan distance of pixels:
|
||||
/// <see>
|
||||
/// <cref>https://en.wikipedia.org/wiki/Taxicab_geometry</cref>
|
||||
/// </see>
|
||||
/// </summary>
|
||||
public int PerPixelManhattanThreshold { get; }
|
||||
|
||||
public override ImageSimilarityReport<TPixelA, TPixelB> CompareImagesOrFrames<TPixelA, TPixelB>(ImageFrame<TPixelA> expected, ImageFrame<TPixelB> actual)
|
||||
{
|
||||
if (expected.Size() != actual.Size())
|
||||
{
|
||||
throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!");
|
||||
}
|
||||
|
||||
int width = actual.Width;
|
||||
|
||||
// TODO: Comparing through Rgba64 may not robust enough because of the existance of super high precision pixel types.
|
||||
|
||||
var aBuffer = new Rgba32[width];
|
||||
var bBuffer = new Rgba32[width];
|
||||
|
||||
float totalDifference = 0F;
|
||||
|
||||
var differences = new List<PixelDifference>();
|
||||
|
||||
for (int y = 0; y < actual.Height; y++)
|
||||
{
|
||||
Span<TPixelA> aSpan = expected.GetPixelRowSpan(y);
|
||||
Span<TPixelB> bSpan = actual.GetPixelRowSpan(y);
|
||||
|
||||
for (var i = 0; i < aSpan.Length; i++) { aSpan[i].ToRgba32(ref aBuffer[i]); }
|
||||
for (var i = 0; i < bSpan.Length; i++) { bSpan[i].ToRgba32(ref bBuffer[i]); }
|
||||
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
int d = GetManhattanDistanceInRgbaSpace(ref aBuffer[x], ref bBuffer[x]);
|
||||
|
||||
if (d > this.PerPixelManhattanThreshold)
|
||||
{
|
||||
var diff = new PixelDifference(new Point(x, y), aBuffer[x], bBuffer[x]);
|
||||
differences.Add(diff);
|
||||
|
||||
totalDifference += d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float normalizedDifference = totalDifference / (actual.Width * (float)actual.Height);
|
||||
normalizedDifference /= 4F * 65535F;
|
||||
|
||||
if (normalizedDifference > this.ImageThreshold)
|
||||
{
|
||||
return new ImageSimilarityReport<TPixelA, TPixelB>(expected, actual, differences, normalizedDifference);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ImageSimilarityReport<TPixelA, TPixelB>.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int GetManhattanDistanceInRgbaSpace(ref Rgba32 a, ref Rgba32 b)
|
||||
{
|
||||
return Diff(a.R, b.R) + Diff(a.G, b.G) + Diff(a.B, b.B) + Diff(a.A, b.A);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int Diff(ushort a, ushort b) => Math.Abs(a - b);
|
||||
}
|
||||
}
|
|
@ -1,20 +1,26 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Xunit;
|
||||
|
||||
namespace EQL.SVGSharpie.ImageSharp.Tests
|
||||
{
|
||||
public class UnitTests
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
string path = Path.GetTempFileName();
|
||||
string testData = Guid.NewGuid().ToString();
|
||||
File.WriteAllText(path, testData);
|
||||
string data = File.ReadAllText(path);
|
||||
Assert.Equal(testData, data);
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.IO;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
|
||||
using Xunit;
|
||||
|
||||
namespace EquinoxLabs.SVGSharpie.ImageSharp.Tests
|
||||
{
|
||||
public class UnitTests
|
||||
{
|
||||
public static TheoryData<string, string, string> SVG11Tests => Utils.GetTestImages(Path.Combine(Utils.TestFolder, "SVG1.1"));
|
||||
public static TheoryData<string, string, string> SVG12Tests => Utils.GetTestImages(Path.Combine(Utils.TestFolder, "SVG1.2"));
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SVG11Tests))]
|
||||
public void SvgTest(string svgFilePath, string pngFilePath, string resultFilePath)
|
||||
{
|
||||
using (Image<Rgba32> svgImg = SvgImageRenderer.RenderFromString<Rgba32>(File.ReadAllText(svgFilePath)))
|
||||
using (var pngImg = Image.Load(pngFilePath))
|
||||
{
|
||||
svgImg.Save(resultFilePath);
|
||||
ImageComparer.Tolerant(perPixelManhattanThreshold: 500).VerifySimilarity(svgImg, pngImg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace EquinoxLabs.SVGSharpie.ImageSharp.Tests
|
||||
{
|
||||
public static class Utils
|
||||
{
|
||||
public static readonly string TestFolder;
|
||||
|
||||
static Utils()
|
||||
{
|
||||
var rootpath = Path.GetDirectoryName(new Uri(typeof(Utils).GetTypeInfo().Assembly.CodeBase).LocalPath);
|
||||
TestFolder = Path.GetFullPath(Path.Combine(rootpath, "..", "..", "..", "..", "SVGTests"));
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, string> GetTestImages(string folder)
|
||||
{
|
||||
var result = new TheoryData<string, string, string>();
|
||||
|
||||
var svgFolder = Path.Combine(folder, "svg");
|
||||
var pngFolder = Path.Combine(folder, "png");
|
||||
var resultFolder = Path.Combine(folder, "result");
|
||||
|
||||
IEnumerable<string> svgFiles = Directory.EnumerateFiles(svgFolder, "*.svg");
|
||||
foreach (string svgFile in svgFiles)
|
||||
{
|
||||
var filename = Path.GetFileNameWithoutExtension(svgFile);
|
||||
var pngFile = Path.Combine(pngFolder, $"{filename}.png");
|
||||
var resultFile = Path.Combine(resultFolder, $"{filename}.png");
|
||||
result.Add(svgFile, pngFile, resultFile);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//private static string FindParentFolderContaining(string item, string path = ".")
|
||||
//{
|
||||
// IEnumerable<string> items = Directory.EnumerateFileSystemEntries(path).Select(Path.GetFileName);
|
||||
// if (items.Any(x => x.Equals(item, StringComparison.OrdinalIgnoreCase)))
|
||||
// {
|
||||
// return Path.GetFullPath(path);
|
||||
// }
|
||||
// string parent = Path.GetDirectoryName(Path.GetFullPath(path));
|
||||
// if (parent != null)
|
||||
// {
|
||||
// return FindParentFolderContaining(item, parent);
|
||||
// }
|
||||
// return null;
|
||||
//}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
*.png
|
Загрузка…
Ссылка в новой задаче