mirror of https://github.com/SixLabors/ImageSharp
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