mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
8 changed files with 380 additions and 63 deletions
@ -0,0 +1,166 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Numerics; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Transforms |
|||
{ |
|||
/// <summary>
|
|||
/// Enumerates the various options which determine which side to taper
|
|||
/// </summary>
|
|||
public enum TaperSide |
|||
{ |
|||
/// <summary>
|
|||
/// Taper the left side
|
|||
/// </summary>
|
|||
Left, |
|||
|
|||
/// <summary>
|
|||
/// Taper the top side
|
|||
/// </summary>
|
|||
Top, |
|||
|
|||
/// <summary>
|
|||
/// Taper the right side
|
|||
/// </summary>
|
|||
Right, |
|||
|
|||
/// <summary>
|
|||
/// Taper the bottom side
|
|||
/// </summary>
|
|||
Bottom |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Enumerates the various options which determine how to taper corners
|
|||
/// </summary>
|
|||
public enum TaperCorner |
|||
{ |
|||
/// <summary>
|
|||
/// Taper the left or top corner
|
|||
/// </summary>
|
|||
LeftOrTop, |
|||
|
|||
/// <summary>
|
|||
/// Taper the right or bottom corner
|
|||
/// </summary>
|
|||
RightOrBottom, |
|||
|
|||
/// <summary>
|
|||
/// Taper the both sets of corners
|
|||
/// </summary>
|
|||
Both |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Provides helper methods for working with generalized projective transforms.
|
|||
/// </summary>
|
|||
public static class ProjectiveTransformHelper |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a matrix that performs a tapering projective transform.
|
|||
/// <see href="https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/transforms/non-affine"/>
|
|||
/// </summary>
|
|||
/// <param name="size">The rectangular size of the image being transformed.</param>
|
|||
/// <param name="taperSide">An enumeration that indicates the side of the rectangle that tapers.</param>
|
|||
/// <param name="taperCorner">An enumeration that indicates on which corners to taper the rectangle.</param>
|
|||
/// <param name="taperFraction">The amount to taper.</param>
|
|||
/// <returns>The <see cref="Matrix4x4"/></returns>
|
|||
public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide taperSide, TaperCorner taperCorner, float taperFraction) |
|||
{ |
|||
Matrix4x4 matrix = Matrix4x4.Identity; |
|||
|
|||
switch (taperSide) |
|||
{ |
|||
case TaperSide.Left: |
|||
matrix.M11 = taperFraction; |
|||
matrix.M22 = taperFraction; |
|||
matrix.M13 = (taperFraction - 1) / size.Width; |
|||
|
|||
switch (taperCorner) |
|||
{ |
|||
case TaperCorner.RightOrBottom: |
|||
break; |
|||
|
|||
case TaperCorner.LeftOrTop: |
|||
matrix.M12 = size.Height * matrix.M13; |
|||
matrix.M32 = size.Height * (1 - taperFraction); |
|||
break; |
|||
|
|||
case TaperCorner.Both: |
|||
matrix.M12 = (size.Height * 0.5f) * matrix.M13; |
|||
matrix.M32 = size.Height * (1 - taperFraction) / 2; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case TaperSide.Top: |
|||
matrix.M11 = taperFraction; |
|||
matrix.M22 = taperFraction; |
|||
matrix.M23 = (taperFraction - 1) / size.Height; |
|||
|
|||
switch (taperCorner) |
|||
{ |
|||
case TaperCorner.RightOrBottom: |
|||
break; |
|||
|
|||
case TaperCorner.LeftOrTop: |
|||
matrix.M21 = size.Width * matrix.M23; |
|||
matrix.M31 = size.Width * (1 - taperFraction); |
|||
break; |
|||
|
|||
case TaperCorner.Both: |
|||
matrix.M21 = (size.Width * 0.5f) * matrix.M23; |
|||
matrix.M31 = size.Width * (1 - taperFraction) / 2; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case TaperSide.Right: |
|||
matrix.M11 = 1 / taperFraction; |
|||
matrix.M13 = (1 - taperFraction) / (size.Width * taperFraction); |
|||
|
|||
switch (taperCorner) |
|||
{ |
|||
case TaperCorner.RightOrBottom: |
|||
break; |
|||
|
|||
case TaperCorner.LeftOrTop: |
|||
matrix.M12 = size.Height * matrix.M13; |
|||
break; |
|||
|
|||
case TaperCorner.Both: |
|||
matrix.M12 = (size.Height * 0.5f) * matrix.M13; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case TaperSide.Bottom: |
|||
matrix.M22 = 1 / taperFraction; |
|||
matrix.M23 = (1 - taperFraction) / (size.Height * taperFraction); |
|||
|
|||
switch (taperCorner) |
|||
{ |
|||
case TaperCorner.RightOrBottom: |
|||
break; |
|||
|
|||
case TaperCorner.LeftOrTop: |
|||
matrix.M21 = size.Width * matrix.M23; |
|||
break; |
|||
|
|||
case TaperCorner.Both: |
|||
matrix.M21 = (size.Width * 0.5f) * matrix.M23; |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
return matrix; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,137 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Numerics; |
|||
using System.Reflection; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.ImageSharp.Processing.Transforms; |
|||
using SixLabors.ImageSharp.Processing.Transforms.Resamplers; |
|||
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
|||
using Xunit; |
|||
// ReSharper disable InconsistentNaming
|
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Processing.Transforms |
|||
{ |
|||
using Xunit.Abstractions; |
|||
|
|||
public class ProjectiveTransformTests |
|||
{ |
|||
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.03f, 3); |
|||
private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.5f, 3); |
|||
|
|||
private ITestOutputHelper Output { get; } |
|||
|
|||
public static readonly TheoryData<string> ResamplerNames = new TheoryData<string> |
|||
{ |
|||
nameof(KnownResamplers.Bicubic), |
|||
nameof(KnownResamplers.Box), |
|||
nameof(KnownResamplers.CatmullRom), |
|||
nameof(KnownResamplers.Hermite), |
|||
nameof(KnownResamplers.Lanczos2), |
|||
nameof(KnownResamplers.Lanczos3), |
|||
nameof(KnownResamplers.Lanczos5), |
|||
nameof(KnownResamplers.Lanczos8), |
|||
nameof(KnownResamplers.MitchellNetravali), |
|||
nameof(KnownResamplers.NearestNeighbor), |
|||
nameof(KnownResamplers.Robidoux), |
|||
nameof(KnownResamplers.RobidouxSharp), |
|||
nameof(KnownResamplers.Spline), |
|||
nameof(KnownResamplers.Triangle), |
|||
nameof(KnownResamplers.Welch), |
|||
}; |
|||
|
|||
public static readonly TheoryData<TaperSide, TaperCorner> TaperMatrixData = new TheoryData<TaperSide, TaperCorner> |
|||
{ |
|||
{ TaperSide.Bottom, TaperCorner.Both }, |
|||
{ TaperSide.Bottom, TaperCorner.LeftOrTop }, |
|||
{ TaperSide.Bottom, TaperCorner.RightOrBottom }, |
|||
|
|||
{ TaperSide.Top, TaperCorner.Both }, |
|||
{ TaperSide.Top, TaperCorner.LeftOrTop }, |
|||
{ TaperSide.Top, TaperCorner.RightOrBottom }, |
|||
|
|||
{ TaperSide.Left, TaperCorner.Both }, |
|||
{ TaperSide.Left, TaperCorner.LeftOrTop }, |
|||
{ TaperSide.Left, TaperCorner.RightOrBottom }, |
|||
|
|||
{ TaperSide.Right, TaperCorner.Both }, |
|||
{ TaperSide.Right, TaperCorner.LeftOrTop }, |
|||
{ TaperSide.Right, TaperCorner.RightOrBottom }, |
|||
|
|||
}; |
|||
|
|||
public ProjectiveTransformTests(ITestOutputHelper output) |
|||
{ |
|||
this.Output = output; |
|||
} |
|||
|
|||
[Theory] |
|||
[WithTestPatternImages(nameof(ResamplerNames), 150, 150, PixelTypes.Rgba32)] |
|||
public void Transform_WithSampler<TPixel>(TestImageProvider<TPixel> provider, string resamplerName) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
IResampler sampler = GetResampler(resamplerName); |
|||
using (Image<TPixel> image = provider.GetImage()) |
|||
{ |
|||
Matrix4x4 m = ProjectiveTransformHelper.CreateTaperMatrix(image.Size(), TaperSide.Right, TaperCorner.Both, .5F); |
|||
|
|||
image.Mutate(i => { i.Transform(m, sampler); }); |
|||
|
|||
image.DebugSave(provider, resamplerName); |
|||
image.CompareToReferenceOutput(ValidatorComparer, provider, resamplerName); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[WithSolidFilledImages(nameof(TaperMatrixData), 30, 30, nameof(Rgba32.Red), PixelTypes.Rgba32)] |
|||
public void Transform_WithTaperMatrix<TPixel>(TestImageProvider<TPixel> provider, TaperSide taperSide, TaperCorner taperCorner) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
using (Image<TPixel> image = provider.GetImage()) |
|||
{ |
|||
Matrix4x4 m = ProjectiveTransformHelper.CreateTaperMatrix(image.Size(), taperSide, taperCorner, .5F); |
|||
image.Mutate(i => { i.Transform(m); }); |
|||
|
|||
string testOutputDetails = $"{taperSide}-{taperCorner}"; |
|||
image.DebugSave(provider, testOutputDetails); |
|||
image.CompareFirstFrameToReferenceOutput(TolerantComparer, provider, testOutputDetails); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[WithSolidFilledImages(100, 100, 0, 0, 255, PixelTypes.Rgba32)] |
|||
public void RawTransformMatchesDocumentedExample<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
// Printing some extra output to help investigating roundoff errors:
|
|||
this.Output.WriteLine($"Vector.IsHardwareAccelerated: {Vector.IsHardwareAccelerated}"); |
|||
|
|||
// This test matches the output described in the example at
|
|||
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/transforms/non-affine
|
|||
using (Image<TPixel> image = provider.GetImage()) |
|||
{ |
|||
Matrix4x4 m = Matrix4x4.Identity; |
|||
m.M13 = 0.01F; |
|||
|
|||
image.Mutate(i => { i.Transform(m); }); |
|||
|
|||
image.DebugSave(provider); |
|||
image.CompareToReferenceOutput(TolerantComparer, provider); |
|||
} |
|||
} |
|||
|
|||
private static IResampler GetResampler(string name) |
|||
{ |
|||
PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name); |
|||
|
|||
if (property == null) |
|||
{ |
|||
throw new Exception("Invalid property name!"); |
|||
} |
|||
|
|||
return (IResampler)property.GetValue(null); |
|||
} |
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
Subproject commit 5a9a88380166a87521d10048f53cda7f5f761d66 |
|||
Subproject commit f641620eb5378db49d6153bbf1443ad13bda2379 |
|||
Loading…
Reference in new issue