Browse Source

Add image comparer test helper

Add a couple of png smoke tests that encode and then decode an image in png then does a visual compare to verify they render the same.
pull/152/head
Scott Williams 9 years ago
parent
commit
e6e97ed404
  1. 5
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  2. 4
      src/ImageSharp/Formats/Png/PngInterlaceMode.cs
  3. 64
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  4. 62
      tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs
  5. 84
      tests/ImageSharp.Tests/ImageComparer.cs
  6. 38
      tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs
  7. 13
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs
  8. 184
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs
  9. 34
      tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs

5
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -545,11 +545,10 @@ namespace ImageSharp.Formats
/// <typeparam name="TColor">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="imageBase">The image base.</param>
private void WritePhysicalChunk<TColor>(Stream stream, ImageBase<TColor> imageBase)
private void WritePhysicalChunk<TColor>(Stream stream, Image<TColor> image)
where TColor : struct, IPixel<TColor>
{
Image<TColor> image = imageBase as Image<TColor>;
if (image != null && image.MetaData.HorizontalResolution > 0 && image.MetaData.VerticalResolution > 0)
if (image.MetaData.HorizontalResolution > 0 && image.MetaData.VerticalResolution > 0)
{
// 39.3700787 = inches in a meter.
int dpmX = (int)Math.Round(image.MetaData.HorizontalResolution * 39.3700787D);

4
src/ImageSharp/Formats/Png/PngInterlaceMode.cs

@ -13,11 +13,11 @@ namespace ImageSharp.Formats
/// <summary>
/// Non interlaced
/// </summary>
None,
None = 0,
/// <summary>
/// Adam 7 interlacing.
/// </summary>
Adam7
Adam7 = 1
}
}

64
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -9,12 +9,12 @@ namespace ImageSharp.Tests
{
using System.IO;
using System.Threading.Tasks;
using ImageSharp.IO;
using Xunit;
public class PngEncoderTests : FileTestBase
{
[Fact]
[Fact(Skip ="Slow intergration test")]
public void ImageCanSaveIndexedPng()
{
string path = CreateOutputDirectory("Png", "Indexed");
@ -32,7 +32,7 @@ namespace ImageSharp.Tests
}
}
[Fact]
[Fact(Skip = "Slow intergration test")]
public void ImageCanSavePngInParallel()
{
string path = this.CreateOutputDirectory("Png");
@ -50,5 +50,63 @@ namespace ImageSharp.Tests
}
});
}
[Theory]
[WithBlankImages(1, 1, PixelTypes.All)]
public void WritesFileMarker<TColor>(TestImageProvider<TColor> provider)
where TColor : struct, IPixel<TColor>
{
using (Image<TColor> image = provider.GetImage())
using (EndianBinaryReader reader = Encode(image, null))
{
byte[] data = reader.ReadBytes(8);
byte[] expected = {
0x89, // Set the high bit.
0x50, // P
0x4E, // N
0x47, // G
0x0D, // Line ending CRLF
0x0A, // Line ending CRLF
0x1A, // EOF
0x0A // LF
};
Assert.Equal(expected, data);
}
}
[Theory]
[WithBlankImages(1, 1, PixelTypes.All)]
[WithBlankImages(10, 10, PixelTypes.StandardImageClass)]
public void WritesFileHeaderHasHeightAndWidth<TColor>(TestImageProvider<TColor> provider)
where TColor : struct, IPixel<TColor>
{
using (Image<TColor> image = provider.GetImage())
using (EndianBinaryReader reader = Encode(image, null))
{
reader.ReadBytes(8); // throw away the file header
uint width = reader.ReadUInt32();
uint height = reader.ReadUInt32();
byte bitDepth = reader.ReadByte();
byte colorType = reader.ReadByte();
byte compressionMethod = reader.ReadByte();
byte filterMethod = reader.ReadByte();
byte interlaceMethod = reader.ReadByte();
Assert.Equal(image.Width, (int)width);
Assert.Equal(image.Height, (int)height);
}
}
private static EndianBinaryReader Encode<TColor>(Image<TColor> img, IEncoderOptions options)
where TColor : struct, IPixel<TColor>
{
MemoryStream stream = new MemoryStream();
new PngEncoder().Encode(img, stream, null);
stream.Position = 0;
return new EndianBinaryReader(Endianness.BigEndian, stream);
}
}
}

62
tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs

@ -0,0 +1,62 @@
// <copyright file="PngSmokeTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests.Formats.Png
{
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using Xunit;
using ImageSharp.Formats;
using System.Linq;
using ImageSharp.IO;
public class PngSmokeTests
{
[Theory]
[WithTestPatternImages(300, 300, PixelTypes.All)]
public void WritesFileMarker<TColor>(TestImageProvider<TColor> provider)
where TColor : struct, IPixel<TColor>
{
// does saving a file then repoening mean both files are identical???
using (Image<TColor> image = provider.GetImage())
using (MemoryStream ms = new MemoryStream())
{
image.Save(provider.Utility.GetTestOutputFileName("bmp"));
image.Save(ms, new PngEncoder());
ms.Position = 0;
using (Image img2 = new Image(ms, new Configuration(new PngFormat())))
{
img2.Save(provider.Utility.GetTestOutputFileName("bmp", "_loaded"), new BmpEncoder());
ImageComparer.VisualComparer(image, img2);
}
}
}
[Theory]
[WithTestPatternImages(300, 300, PixelTypes.All)]
public void Resize<TColor>(TestImageProvider<TColor> provider)
where TColor : struct, IPixel<TColor>
{
// does saving a file then repoening mean both files are identical???
using (Image<TColor> image = provider.GetImage())
using (MemoryStream ms = new MemoryStream())
{
// image.Save(provider.Utility.GetTestOutputFileName("png"));
image.Resize(100, 100);
// image.Save(provider.Utility.GetTestOutputFileName("png", "resize"));
image.Save(ms, new PngEncoder());
ms.Position = 0;
using (Image img2 = new Image(ms, new Configuration(new PngFormat())))
{
ImageComparer.VisualComparer(image, img2);
}
}
}
}
}

84
tests/ImageSharp.Tests/ImageComparer.cs

@ -0,0 +1,84 @@
namespace ImageSharp.Tests
{
using System;
using ImageSharp;
using Xunit;
/// <summary>
/// Class to perform simple image comparisons.
/// </summary>
public static class ImageComparer
{
const int DefaultScalingFactor = 32;
const int DefaultSegmentThreshold = 3;
const float DefaultImageThreshold = 0.000f;
public static void VisualComparer<TColorA, TColorB>(Image<TColorA> expected, Image<TColorB> actual, float imageTheshold = DefaultImageThreshold, byte segmentThreshold = DefaultSegmentThreshold, int scalingFactor = DefaultScalingFactor)
where TColorA : struct, IPixel<TColorA>
where TColorB : struct, IPixel<TColorB>
{
var percentage = expected.PercentageDifference(actual, segmentThreshold, scalingFactor);
Assert.InRange(percentage, 0, imageTheshold);
}
public static float PercentageDifference<TColorA, TColorB>(this Image<TColorA> source, Image<TColorB> target, byte segmentThreshold = DefaultSegmentThreshold, int scalingFactor = DefaultScalingFactor)
where TColorA : struct, IPixel<TColorA>
where TColorB : struct, IPixel<TColorB>
{
// code adapted from https://www.codeproject.com/Articles/374386/Simple-image-comparison-in-NET
Fast2DArray<byte> differences = GetDifferences(source, target, scalingFactor);
int diffPixels = 0;
foreach (byte b in differences.Data)
{
if (b > segmentThreshold) { diffPixels++; }
}
return diffPixels / (scalingFactor * scalingFactor);
}
private static Fast2DArray<byte> GetDifferences<TColorA, TColorB>(Image<TColorA> source, Image<TColorB> target, int scalingFactor)
where TColorA : struct, IPixel<TColorA>
where TColorB : struct, IPixel<TColorB>
{
Fast2DArray<byte> differences = new Fast2DArray<byte>(scalingFactor, scalingFactor);
Fast2DArray<byte> firstGray = source.GetGrayScaleValues(scalingFactor);
Fast2DArray<byte> secondGray = target.GetGrayScaleValues(scalingFactor);
for (int y = 0; y < scalingFactor; y++)
{
for (int x = 0; x < scalingFactor; x++)
{
differences[x, y] = (byte)Math.Abs(firstGray[x, y] - secondGray[x, y]);
}
}
return differences;
}
private static Fast2DArray<byte> GetGrayScaleValues<TColorA>(this Image<TColorA> source, int scalingFactor)
where TColorA : struct, IPixel<TColorA>
{
byte[] buffer = new byte[4];
using (Image<TColorA> img = new Image<TColorA>(source).Resize(scalingFactor, scalingFactor).Grayscale())
{
using (PixelAccessor<TColorA> pixels = img.Lock())
{
Fast2DArray<byte> grayScale = new Fast2DArray<byte>(scalingFactor, scalingFactor);
for (int y = 0; y < scalingFactor; y++)
{
for (int x = 0; x < scalingFactor; x++)
{
pixels[x, y].ToXyzBytes(buffer, 0);
grayScale[x, y] = buffer[1];
}
}
return grayScale;
}
}
}
}
}

38
tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs

@ -0,0 +1,38 @@
// <copyright file="WithBlankImagesAttribute.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests
{
using System;
using System.Reflection;
/// <summary>
/// Triggers passing <see cref="TestImageProvider{TColor}"/> instances which produce a blank image of size width * height.
/// One <see cref="TestImageProvider{TColor}"/> instance will be passed for each the pixel format defined by the pixelTypes parameter
/// </summary>
public class WithTestPatternImagesAttribute : ImageDataAttributeBase
{
/// <summary>
/// Triggers passing an <see cref="TestImageProvider{TColor}"/> that produces a test pattern image of size width * height
/// </summary>
/// <param name="width">The required width</param>
/// <param name="height">The required height</param>
/// <param name="pixelTypes">The requested parameter</param>
/// <param name="additionalParameters">Additional theory parameter values</param>
public WithTestPatternImagesAttribute(int width, int height, PixelTypes pixelTypes, params object[] additionalParameters)
: base(pixelTypes, additionalParameters)
{
this.Width = width;
this.Height = height;
}
public int Width { get; }
public int Height { get; }
protected override string GetFactoryMethodName(MethodInfo testMethod) => "TestPattern";
protected override object[] GetFactoryMethodArgs(MethodInfo testMethod, Type factoryType) => new object[] { this.Width, this.Height };
}
}

13
tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs

@ -25,13 +25,20 @@ namespace ImageSharp.Tests
public ImagingTestCaseUtility Utility { get; private set; }
public GenericFactory<TColor> Factory { get; private set; } = new GenericFactory<TColor>();
public static TestImageProvider<TColor> Blank(
public static TestImageProvider<TColor> TestPattern(
int width,
int height,
MethodInfo testMethod = null,
PixelTypes pixelTypeOverride = PixelTypes.Undefined)
=> new BlankProvider(width, height).Init(testMethod, pixelTypeOverride);
=> new TestPatternProvider(width, height).Init(testMethod, pixelTypeOverride);
public static TestImageProvider<TColor> Blank(
int width,
int height,
MethodInfo testMethod = null,
PixelTypes pixelTypeOverride = PixelTypes.Undefined)
=> new BlankProvider(width, height).Init(testMethod, pixelTypeOverride);
public static TestImageProvider<TColor> File(
string filePath,

184
tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs

@ -0,0 +1,184 @@
// <copyright file="BlankProvider.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests
{
using System;
using System.Collections.Generic;
using System.Numerics;
public abstract partial class TestImageProvider<TColor>
where TColor : struct, IPixel<TColor>
{
private class TestPatternProvider : TestImageProvider<TColor>
{
static Dictionary<string, Image<TColor>> testImages = new Dictionary<string, Image<TColor>>();
public TestPatternProvider(int width, int height)
{
this.Width = width;
this.Height = height;
}
public override string SourceFileOrDescription => $"TestPattern{this.Width}x{this.Height}";
protected int Height { get; }
protected int Width { get; }
public override Image<TColor> GetImage()
{
lock (testImages)
{
if (!testImages.ContainsKey(this.SourceFileOrDescription))
{
var image = new Image<TColor>(this.Width, this.Height);
DrawTestPattern(image);
testImages.Add(this.SourceFileOrDescription, image);
}
return new Image<TColor>(testImages[this.SourceFileOrDescription]);
}
}
private static void DrawTestPattern(Image<TColor> image)
{
// first lets split the image into 4 quadrants
using (var pixels = image.Lock())
{
BlackWhiteChecker(pixels); // top left
HorizontalLines(pixels); // top right
TransparentGradients(pixels); // bottom left
Raninbow(pixels); // bottom right
}
}
private static void HorizontalLines(PixelAccessor<TColor> pixels)
{
// topLeft
int left = pixels.Width / 2;
int right = pixels.Width;
int top = 0;
int bottom = pixels.Height / 2;
int stride = pixels.Width / 12;
TColor[] c = {
NamedColors<TColor>.HotPink,
NamedColors<TColor>.Blue
};
int p = 0;
for (var y = top; y < bottom; y++)
{
for (var x = left; x < right; x++)
{
if (x % stride == 0)
{
p++;
p = p % c.Length;
}
pixels[x, y] = c[p];
}
}
}
private static void BlackWhiteChecker(PixelAccessor<TColor> pixels)
{
// topLeft
int left = 0;
int right = pixels.Width / 2;
int top = 0;
int bottom = pixels.Height / 2;
int stride = pixels.Width / 6;
TColor[] c = {
NamedColors<TColor>.Black,
NamedColors<TColor>.White
};
int p = 0;
for (var y = top; y < bottom; y++)
{
if (y % stride == 0)
{
p++;
p = p % c.Length;
}
var pstart = p;
for (var x = left; x < right; x++)
{
if (x % stride == 0)
{
p++;
p = p % c.Length;
}
pixels[x, y] = c[p];
}
p = pstart;
}
}
private static void TransparentGradients(PixelAccessor<TColor> pixels)
{
// topLeft
int left = 0;
int right = pixels.Width / 2;
int top = pixels.Height / 2;
int bottom = pixels.Height;
int height = (int)Math.Ceiling(pixels.Height / 6f);
Vector4 red = Color.Red.ToVector4(); // use real color so we can see har it translates in the test pattern
Vector4 green = Color.Green.ToVector4(); // use real color so we can see har it translates in the test pattern
Vector4 blue = Color.Blue.ToVector4(); // use real color so we can see har it translates in the test pattern
TColor c = default(TColor);
for (var x = left; x < right; x++)
{
blue.W = red.W = green.W = (float)x / (float)right;
c.PackFromVector4(red);
var topBand = top;
for (var y = topBand; y < top + height; y++)
{
pixels[x, y] = c;
}
topBand = topBand + height;
c.PackFromVector4(green);
for (var y = topBand; y < topBand + height; y++)
{
pixels[x, y] = c;
}
topBand = topBand + height;
c.PackFromVector4(blue);
for (var y = topBand; y < bottom; y++)
{
pixels[x, y] = c;
}
}
}
private static void Raninbow(PixelAccessor<TColor> pixels)
{
int left = pixels.Width / 2;
int right = pixels.Width;
int top = pixels.Height / 2;
int bottom = pixels.Height;
int pixelCount = left * top;
uint stepsPerPixel = (uint)(uint.MaxValue / pixelCount);
TColor c = default(TColor);
Color t = new Color(0);
uint inital = 0;
for (var x = left; x < right; x++)
for (var y = top; y < bottom; y++)
{
t.PackedValue += stepsPerPixel;
var v = t.ToVector4();
//v.W = (x - left) / (float)left;
c.PackFromVector4(v);
pixels[x, y] = c;
}
}
}
}
}

34
tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs

@ -44,13 +44,26 @@ namespace ImageSharp.Tests
/// </summary>
/// <param name="extension"></param>
/// <returns>The required extension</returns>
public string GetTestOutputFileName(string extension = null)
public string GetTestOutputFileName(string extension = null, string tag = null)
{
string fn = string.Empty;
if (string.IsNullOrWhiteSpace(extension))
{
extension = null;
}
fn = Path.GetFileNameWithoutExtension(this.SourceFileOrDescription);
extension = extension ?? Path.GetExtension(this.SourceFileOrDescription);
extension = extension ?? ".bmp";
if (string.IsNullOrWhiteSpace(extension))
{
extension = Path.GetExtension(this.SourceFileOrDescription);
}
if (string.IsNullOrWhiteSpace(extension))
{
extension = ".bmp";
}
if (extension[0] != '.')
{
@ -65,7 +78,14 @@ namespace ImageSharp.Tests
pixName = '_' + pixName;
}
return $"{this.GetTestOutputDir()}/{this.TestName}{pixName}{fn}{extension}";
tag = tag ?? string.Empty;
if (tag != string.Empty)
{
tag= '_' + tag;
}
return $"{this.GetTestOutputDir()}/{this.TestName}{pixName}{fn}{tag}{extension}";
}
/// <summary>
@ -80,7 +100,7 @@ namespace ImageSharp.Tests
where TColor : struct, IPixel<TColor>
{
string path = this.GetTestOutputFileName(extension);
extension = Path.GetExtension(path);
IImageFormat format = GetImageFormatByExtension(extension);
encoder = encoder ?? format.Encoder;
@ -99,8 +119,8 @@ namespace ImageSharp.Tests
private static IImageFormat GetImageFormatByExtension(string extension)
{
extension = extension.ToLower();
return Configuration.Default.ImageFormats.First(f => f.SupportedExtensions.Contains(extension));
extension = extension?.TrimStart('.');
return Configuration.Default.ImageFormats.First(f => f.SupportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase));
}
private string GetTestOutputDir()

Loading…
Cancel
Save