Browse Source

Introduce ProjectiveTransformBuilder

af/merge-core
James Jackson-South 7 years ago
parent
commit
d786c6e53e
  1. 10
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  2. 6
      src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs
  3. 40
      src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs
  4. 116
      src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs
  5. 197
      src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs
  6. 138
      src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs
  7. 2
      src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs
  8. 58
      src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs
  9. 162
      src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
  10. 113
      src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
  11. 166
      src/ImageSharp/Processing/ProjectiveTransformHelper.cs
  12. 26
      src/ImageSharp/Processing/TaperCorner.cs
  13. 31
      src/ImageSharp/Processing/TaperSide.cs
  14. 40
      src/ImageSharp/Processing/TransformExtensions.cs
  15. 2
      tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs
  16. 27
      tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs
  17. 2
      tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs

10
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -30,6 +30,11 @@ namespace SixLabors.ImageSharp.Processing
: this(sourceRectangle.Size)
=> this.rectangle = sourceRectangle;
/// <summary>
/// Gets the source image size.
/// </summary>
internal Size Size { get; }
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees.
/// </summary>
@ -41,11 +46,6 @@ namespace SixLabors.ImageSharp.Processing
return this;
}
/// <summary>
/// Gets the source image size.
/// </summary>
internal Size Size { get; }
/// <summary>
/// Appends a centered rotation matrix using the given rotation in degrees.
/// </summary>

6
src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs

@ -22,9 +22,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="matrix">The transform matrix.</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
/// <param name="sourceSize">The source image size</param>
/// <param name="sourceSize">The source image size.</param>
public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize)
{
Guard.NotNull(sampler, nameof(sampler));
@ -67,7 +67,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
Configuration configuration)
{
// Handle tranforms that result in output identical to the original.
if (this.TransformMatrix.Equals(Matrix3x2.Identity))
if (this.TransformMatrix.Equals(default) || this.TransformMatrix.Equals(Matrix3x2.Identity))
{
// The cloned will be blank here copy all the pixel data over
source.GetPixelSpan().CopyTo(destination.GetPixelSpan());

40
src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs

@ -1,40 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// A base class that provides methods to allow the automatic centering of non-affine transforms
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class CenteredProjectiveTransformProcessor<TPixel> : ProjectiveTransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="CenteredProjectiveTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
/// <param name="sourceSize">The source image size</param>
protected CenteredProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size sourceSize)
: base(matrix, sampler, GetTransformedDimensions(sourceSize, matrix))
{
}
/// <inheritdoc/>
protected override Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle)
{
return TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix);
}
private static Size GetTransformedDimensions(Size sourceDimensions, Matrix4x4 matrix)
{
var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height);
return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix).Size;
}
}
}

116
src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs

@ -1,116 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The base class for performing interpolated affine and non-affine transforms.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class InterpolatedTransformProcessorBase<TPixel> : TransformProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="InterpolatedTransformProcessorBase{TPixel}"/> class.
/// </summary>
/// <param name="sampler">The sampler to perform the transform operation.</param>
protected InterpolatedTransformProcessorBase(IResampler sampler)
{
Guard.NotNull(sampler, nameof(sampler));
this.Sampler = sampler;
}
/// <summary>
/// Gets the sampler to perform interpolation of the transform operation.
/// </summary>
public IResampler Sampler { get; }
/// <summary>
/// Calculated the weights for the given point.
/// This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered.
/// Additionally the weights are normalized.
/// </summary>
/// <param name="min">The minimum sampling offset</param>
/// <param name="max">The maximum sampling offset</param>
/// <param name="sourceMin">The minimum source bounds</param>
/// <param name="sourceMax">The maximum source bounds</param>
/// <param name="point">The transformed point dimension</param>
/// <param name="sampler">The sampler</param>
/// <param name="scale">The transformed image scale relative to the source</param>
/// <param name="weightsRef">The reference to the collection of weights</param>
/// <param name="length">The length of the weights collection</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, ref float weightsRef, int length)
{
float sum = 0;
// Downsampling weights requires more edge sampling plus normalization of the weights
for (int x = 0, i = min; i <= max; i++, x++)
{
int index = i;
if (index < sourceMin)
{
index = sourceMin;
}
if (index > sourceMax)
{
index = sourceMax;
}
float weight = sampler.GetValue((index - point) / scale);
sum += weight;
Unsafe.Add(ref weightsRef, x) = weight;
}
if (sum > 0)
{
for (int i = 0; i < length; i++)
{
ref float wRef = ref Unsafe.Add(ref weightsRef, i);
wRef /= sum;
}
}
}
/// <summary>
/// Calculated the weights for the given point.
/// </summary>
/// <param name="sourceMin">The minimum source bounds</param>
/// <param name="sourceMax">The maximum source bounds</param>
/// <param name="point">The transformed point dimension</param>
/// <param name="sampler">The sampler</param>
/// <param name="weightsRef">The reference to the collection of weights</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, ref float weightsRef)
{
for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++)
{
Unsafe.Add(ref weightsRef, x) = sampler.GetValue(i - point);
}
}
/// <summary>
/// Calculates the sampling radius for the current sampler
/// </summary>
/// <param name="sourceSize">The source dimension size</param>
/// <param name="destinationSize">The destination dimension size</param>
/// <returns>The radius, and scaling factor</returns>
protected (float radius, float scale, float ratio) GetSamplingRadius(int sourceSize, int destinationSize)
{
float ratio = (float)sourceSize / destinationSize;
float scale = ratio;
if (scale < 1F)
{
scale = 1F;
}
return (MathF.Ceiling(scale * this.Sampler.Radius), scale, ratio);
}
}
}

197
src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs

@ -5,13 +5,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -20,22 +16,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// Provides the base methods to perform non-affine transforms on an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class ProjectiveTransformProcessor<TPixel> : InterpolatedTransformProcessorBase<TPixel>
internal class ProjectiveTransformProcessor<TPixel> : TransformProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="matrix">The transform matrix.</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
/// <param name="targetDimensions">The target dimensions to constrain the transformed image to.</param>
public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size targetDimensions)
: base(sampler)
/// <param name="sourceSize">The source image size.</param>
public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size sourceSize)
{
Guard.NotNull(sampler, nameof(sampler));
this.Sampler = sampler;
this.TransformMatrix = matrix;
this.TargetDimensions = targetDimensions;
this.TargetDimensions = TransformUtils.GetTransformedSize(sourceSize, matrix);
}
/// <summary>
/// Gets the sampler to perform interpolation of the transform operation.
/// </summary>
public IResampler Sampler { get; }
/// <summary>
/// Gets the matrix used to supply the projective transform
/// </summary>
@ -60,17 +62,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Rectangle sourceRectangle, Configuration configuration)
{
// Handle tranforms that result in output identical to the original.
if (this.TransformMatrix.Equals(default) || this.TransformMatrix.Equals(Matrix4x4.Identity))
{
// The cloned will be blank here copy all the pixel data over
source.GetPixelSpan().CopyTo(destination.GetPixelSpan());
return;
}
int height = this.TargetDimensions.Height;
int width = this.TargetDimensions.Width;
Rectangle sourceBounds = source.Bounds();
var targetBounds = new Rectangle(0, 0, width, height);
// Since could potentially be resizing the canvas we might need to re-calculate the matrix
Matrix4x4 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds);
var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions);
// Convert from screen to world space.
Matrix4x4.Invert(matrix, out matrix);
Matrix4x4.Invert(this.TransformMatrix, out Matrix4x4 matrix);
const float Epsilon = 0.0000001F;
if (this.Sampler is NearestNeighborResampler)
@ -92,7 +98,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int px = (int)MathF.Round(v3.X / z);
int py = (int)MathF.Round(v3.Y / z);
if (sourceBounds.Contains(px, py))
if (sourceRectangle.Contains(px, py))
{
destRow[x] = source[px, py];
}
@ -103,145 +109,40 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return;
}
int maxSourceX = source.Width - 1;
int maxSourceY = source.Height - 1;
(float radius, float scale, float ratio) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width);
(float radius, float scale, float ratio) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height);
float xScale = xRadiusScale.scale;
float yScale = yRadiusScale.scale;
// Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable:
var radius = new Vector4(xRadiusScale.radius, yRadiusScale.radius, 0, 0);
IResampler sampler = this.Sampler;
var maxSource = new Vector4(maxSourceX, maxSourceY, maxSourceX, maxSourceY);
int xLength = (int)MathF.Ceiling((radius.X * 2) + 2);
int yLength = (int)MathF.Ceiling((radius.Y * 2) + 2);
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height))
using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height))
var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler);
try
{
ParallelHelper.IterateRows(
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
targetBounds,
configuration,
rows =>
(rows, vectorBuffer) =>
{
Span<Vector4> vectorSpan = vectorBuffer.Span;
for (int y = rows.Min; y < rows.Max; y++)
{
for (int y = rows.Min; y < rows.Max; y++)
{
ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y));
ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y));
for (int x = 0; x < width; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix);
float z = MathF.Max(v3.Z, Epsilon);
// Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable:
Vector4 point = new Vector4(v3.X, v3.Y, 0, 0) / z;
// Clamp sampling pixel radial extents to the source image edges
Vector4 maxXY = point + radius;
Vector4 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
Span<TPixel> targetRowSpan = destination.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(configuration, targetRowSpan, vectorSpan);
ref float ySpanRef = ref kernel.GetYStartReference(y);
ref float xSpanRef = ref kernel.GetXStartReference(y);
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
// It appears these have to be calculated on-the-fly.
// Precalulating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// I've optimized where I can but am always open to suggestions.
if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(
top,
bottom,
minY,
maxY,
point.Y,
sampler,
yScale,
ref ySpanRef,
yLength);
CalculateWeightsDown(
left,
right,
minX,
maxX,
point.X,
sampler,
xScale,
ref xSpanRef,
xLength);
}
else
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef);
}
// Now multiply the results against the offsets
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float yWeight = Unsafe.Add(ref ySpanRef, yy);
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{
float xWeight = Unsafe.Add(ref xSpanRef, xx);
// Values are first premultiplied to prevent darkening of edge pixels
var current = source[i, j].ToVector4();
Vector4Utils.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
}
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
for (int x = 0; x < width; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix);
Vector2 point = new Vector2(v3.X, v3.Y) / MathF.Max(v3.Z, Epsilon);
// Reverse the premultiplication
Vector4Utils.UnPremultiply(ref sum);
dest.FromVector4(sum);
}
kernel.Convolve(point, x, ref ySpanRef, ref xSpanRef, source.PixelBuffer, vectorSpan);
}
});
PixelOperations<TPixel>.Instance.FromVector4(configuration, vectorSpan, targetRowSpan);
}
});
}
finally
{
kernel.Dispose();
}
}
/// <summary>
/// Gets a transform matrix adjusted for final processing based upon the target image bounds.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <returns>
/// The <see cref="Matrix4x4"/>.
/// </returns>
protected virtual Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) => this.TransformMatrix;
}
}

138
src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs

@ -1,138 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Contains helper methods for working with affine and non-affine transforms
/// </summary>
internal static class TransformHelpers
{
/// <summary>
/// Updates the dimensional metadata of a transformed image
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The image to update</param>
public static void UpdateDimensionalMetData<TPixel>(Image<TPixel> image)
where TPixel : struct, IPixel<TPixel>
{
ExifProfile profile = image.MetaData.ExifProfile;
if (profile is null)
{
return;
}
// Removing the previously stored value allows us to set a value with our own data tag if required.
if (profile.GetValue(ExifTag.PixelXDimension) != null)
{
profile.RemoveValue(ExifTag.PixelXDimension);
if (image.Width <= ushort.MaxValue)
{
profile.SetValue(ExifTag.PixelXDimension, (ushort)image.Width);
}
else
{
profile.SetValue(ExifTag.PixelXDimension, (uint)image.Width);
}
}
if (profile.GetValue(ExifTag.PixelYDimension) != null)
{
profile.RemoveValue(ExifTag.PixelYDimension);
if (image.Height <= ushort.MaxValue)
{
profile.SetValue(ExifTag.PixelYDimension, (ushort)image.Height);
}
else
{
profile.SetValue(ExifTag.PixelYDimension, (uint)image.Height);
}
}
}
/// <summary>
/// Gets the centered transform matrix based upon the source and destination rectangles
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix3x2"/></returns>
public static Matrix3x2 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix3x2 matrix)
{
// We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct.
Matrix3x2.Invert(matrix, out Matrix3x2 inverted);
var translationToTargetCenter = Matrix3x2.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F);
var translateToSourceCenter = Matrix3x2.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F);
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
// Translate back to world to pass to the Transform method.
return centered;
}
/// <summary>
/// Gets the centered transform matrix based upon the source and destination rectangles
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix4x4 matrix)
{
// We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct.
Matrix4x4.Invert(matrix, out Matrix4x4 inverted);
var translationToTargetCenter = Matrix4x4.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F, 0);
var translateToSourceCenter = Matrix4x4.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F, 0);
Matrix4x4.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix4x4 centered);
// Translate back to world to pass to the Transform method.
return centered;
}
/// <summary>
/// Returns the bounding rectangle relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix4x4 matrix)
{
// Calculate the position of the four corners in world space by applying
// The world matrix to the four corners in object space (0, 0, width, height)
var tl = Vector2.Transform(Vector2.Zero, matrix);
var tr = Vector2.Transform(new Vector2(rectangle.Width, 0), matrix);
var bl = Vector2.Transform(new Vector2(0, rectangle.Height), matrix);
var br = Vector2.Transform(new Vector2(rectangle.Width, rectangle.Height), matrix);
return GetBoundingRectangle(tl, tr, bl, br);
}
private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br)
{
// Find the minimum and maximum "corners" based on the given vectors
float minX = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X)));
float maxX = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X)));
float minY = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y)));
float maxY = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y)));
float sizeX = maxX - minX + .5F;
float sizeY = maxY - minY + .5F;
return new Rectangle((int)(MathF.Ceiling(minX) - .5F), (int)(MathF.Ceiling(minY) - .5F), (int)MathF.Floor(sizeX), (int)MathF.Floor(sizeY));
}
}
}

2
src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs

@ -16,6 +16,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <inheritdoc/>
protected override void AfterImageApply(Image<TPixel> source, Image<TPixel> destination, Rectangle sourceRectangle)
=> TransformHelpers.UpdateDimensionalMetData(destination);
=> TransformProcessorHelpers.UpdateDimensionalMetData(destination);
}
}

58
src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs

@ -0,0 +1,58 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Contains helper methods for working with transforms.
/// </summary>
internal static class TransformProcessorHelpers
{
/// <summary>
/// Updates the dimensional metadata of a transformed image
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The image to update</param>
public static void UpdateDimensionalMetData<TPixel>(Image<TPixel> image)
where TPixel : struct, IPixel<TPixel>
{
ExifProfile profile = image.MetaData.ExifProfile;
if (profile is null)
{
return;
}
// Removing the previously stored value allows us to set a value with our own data tag if required.
if (profile.GetValue(ExifTag.PixelXDimension) != null)
{
profile.RemoveValue(ExifTag.PixelXDimension);
if (image.Width <= ushort.MaxValue)
{
profile.SetValue(ExifTag.PixelXDimension, (ushort)image.Width);
}
else
{
profile.SetValue(ExifTag.PixelXDimension, (uint)image.Width);
}
}
if (profile.GetValue(ExifTag.PixelYDimension) != null)
{
profile.RemoveValue(ExifTag.PixelYDimension);
if (image.Height <= ushort.MaxValue)
{
profile.SetValue(ExifTag.PixelYDimension, (ushort)image.Height);
}
else
{
profile.SetValue(ExifTag.PixelYDimension, (uint)image.Height);
}
}
}
}
}

162
src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs

@ -58,6 +58,111 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return centered;
}
/// <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="side">An enumeration that indicates the side of the rectangle that tapers.</param>
/// <param name="corner">An enumeration that indicates on which corners to taper the rectangle.</param>
/// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide side, TaperCorner corner, float fraction)
{
Matrix4x4 matrix = Matrix4x4.Identity;
switch (side)
{
case TaperSide.Left:
matrix.M11 = fraction;
matrix.M22 = fraction;
matrix.M13 = (fraction - 1) / size.Width;
switch (corner)
{
case TaperCorner.RightOrBottom:
break;
case TaperCorner.LeftOrTop:
matrix.M12 = size.Height * matrix.M13;
matrix.M32 = size.Height * (1 - fraction);
break;
case TaperCorner.Both:
matrix.M12 = size.Height * .5F * matrix.M13;
matrix.M32 = size.Height * (1 - fraction) / 2;
break;
}
break;
case TaperSide.Top:
matrix.M11 = fraction;
matrix.M22 = fraction;
matrix.M23 = (fraction - 1) / size.Height;
switch (corner)
{
case TaperCorner.RightOrBottom:
break;
case TaperCorner.LeftOrTop:
matrix.M21 = size.Width * matrix.M23;
matrix.M31 = size.Width * (1 - fraction);
break;
case TaperCorner.Both:
matrix.M21 = size.Width * .5F * matrix.M23;
matrix.M31 = size.Width * (1 - fraction) / 2;
break;
}
break;
case TaperSide.Right:
matrix.M11 = 1 / fraction;
matrix.M13 = (1 - fraction) / (size.Width * fraction);
switch (corner)
{
case TaperCorner.RightOrBottom:
break;
case TaperCorner.LeftOrTop:
matrix.M12 = size.Height * matrix.M13;
break;
case TaperCorner.Both:
matrix.M12 = size.Height * .5F * matrix.M13;
break;
}
break;
case TaperSide.Bottom:
matrix.M22 = 1 / fraction;
matrix.M23 = (1 - fraction) / (size.Height * fraction);
switch (corner)
{
case TaperCorner.RightOrBottom:
break;
case TaperCorner.LeftOrTop:
matrix.M21 = size.Width * matrix.M23;
break;
case TaperCorner.Both:
matrix.M21 = size.Width * .5F * matrix.M23;
break;
}
break;
}
return matrix;
}
/// <summary>
/// Returns the rectangle bounds relative to the source for the given transformation matrix.
/// </summary>
@ -114,6 +219,63 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix);
return ConstrainSize(rectangle);
}
/// <summary>
/// Returns the rectangle relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix4x4 matrix)
{
if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix))
{
return rectangle;
}
Vector2 GetVector(float x, float y)
{
const float Epsilon = 0.0000001F;
var v3 = Vector3.Transform(new Vector3(x, y, 1F), matrix);
return new Vector2(v3.X, v3.Y) / MathF.Max(v3.Z, Epsilon);
}
Vector2 tl = GetVector(rectangle.Left, rectangle.Top);
Vector2 tr = GetVector(rectangle.Right, rectangle.Top);
Vector2 bl = GetVector(rectangle.Left, rectangle.Bottom);
Vector2 br = GetVector(rectangle.Right, rectangle.Bottom);
return GetBoundingRectangle(tl, tr, bl, br);
}
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
/// <param name="size">The source size.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Size"/>.
/// </returns>
public static Size GetTransformedSize(Size size, Matrix4x4 matrix)
{
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity))
{
return size;
}
Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix);
return ConstrainSize(rectangle);
}
private static Size ConstrainSize(Rectangle rectangle)
{
// We want to resize the canvas here taking into account any translations.
int height = rectangle.Top < 0 ? rectangle.Bottom : Math.Max(rectangle.Height, rectangle.Bottom);
int width = rectangle.Left < 0 ? rectangle.Right : Math.Max(rectangle.Width, rectangle.Right);

113
src/ImageSharp/Processing/ProjectiveTransformBuilder.cs

@ -0,0 +1,113 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using System.Numerics;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing
{
/// <summary>
/// A helper class for constructing <see cref="Matrix4x4"/> instances for use in projective transforms.
/// </summary>
public class ProjectiveTransformBuilder
{
private readonly List<Matrix4x4> matrices = new List<Matrix4x4>();
private Rectangle rectangle;
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformBuilder"/> class.
/// </summary>
/// <param name="sourceSize">The source image size.</param>
public ProjectiveTransformBuilder(Size sourceSize) => this.Size = sourceSize;
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformBuilder"/> class.
/// </summary>
/// <param name="sourceRectangle">The source rectangle.</param>
public ProjectiveTransformBuilder(Rectangle sourceRectangle)
: this(sourceRectangle.Size)
=> this.rectangle = sourceRectangle;
/// <summary>
/// Gets the source image size.
/// </summary>
internal Size Size { get; }
/// <summary>
/// Prepends a matrix that performs a tapering projective transform.
/// </summary>
/// <param name="side">An enumeration that indicates the side of the rectangle that tapers.</param>
/// <param name="corner">An enumeration that indicates on which corners to taper the rectangle.</param>
/// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependTaperMatrix(TaperSide side, TaperCorner corner, float fraction)
{
this.matrices.Insert(0, TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction));
return this;
}
/// <summary>
/// Appends a matrix that performs a tapering projective transform.
/// </summary>
/// <param name="side">An enumeration that indicates the side of the rectangle that tapers.</param>
/// <param name="corner">An enumeration that indicates on which corners to taper the rectangle.</param>
/// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendTaperMatrix(TaperSide side, TaperCorner corner, float fraction)
{
this.matrices.Add(TransformUtils.CreateTaperMatrix(this.Size, side, corner, fraction));
return this;
}
/// <summary>
/// Prepends a raw matrix.
/// </summary>
/// <param name="matrix">The matrix to prepend.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix)
{
this.matrices.Insert(0, matrix);
return this;
}
/// <summary>
/// Appends a raw matrix.
/// </summary>
/// <param name="matrix">The matrix to append.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix)
{
this.matrices.Add(matrix);
return this;
}
/// <summary>
/// Returns the combined matrix.
/// </summary>
/// <returns>The <see cref="Matrix4x4"/>.</returns>
public Matrix4x4 BuildMatrix()
{
Matrix4x4 matrix = Matrix4x4.Identity;
// Translate the origin matrix to cater for source rectangle offsets.
if (!this.rectangle.Equals(default))
{
matrix *= Matrix4x4.CreateTranslation(new Vector3(-this.rectangle.Location, 0));
}
foreach (Matrix4x4 m in this.matrices)
{
matrix *= m;
}
return matrix;
}
/// <summary>
/// Removes all matrices from the builder.
/// </summary>
public void Clear() => this.matrices.Clear();
}
}

166
src/ImageSharp/Processing/ProjectiveTransformHelper.cs

@ -1,166 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing
{
/// <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;
}
}
}

26
src/ImageSharp/Processing/TaperCorner.cs

@ -0,0 +1,26 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing
{
/// <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
}
}

31
src/ImageSharp/Processing/TaperSide.cs

@ -0,0 +1,31 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing
{
/// <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
}
}

40
src/ImageSharp/Processing/TransformExtensions.cs

@ -1,10 +1,8 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing
{
@ -14,7 +12,7 @@ namespace SixLabors.ImageSharp.Processing
public static class TransformExtensions
{
/// <summary>
/// Transforms an image by the given matrix.
/// Performs an affine transform of an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
@ -25,7 +23,7 @@ namespace SixLabors.ImageSharp.Processing
=> Transform(source, builder, KnownResamplers.Bicubic);
/// <summary>
/// Transforms an image by the given matrix using the specified sampling algorithm.
/// Performs an affine transform of an image using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
@ -37,44 +35,26 @@ namespace SixLabors.ImageSharp.Processing
=> source.ApplyProcessor(new AffineTransformProcessor<TPixel>(builder.BuildMatrix(), sampler, builder.Size));
/// <summary>
/// Transforms an image by the given matrix.
/// Performs a projective transform of an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix)
where TPixel : struct, IPixel<TPixel>
=> Transform(source, matrix, KnownResamplers.Bicubic);
/// <summary>
/// Applies a projective transform to the image by the given matrix using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <param name="builder">The affine transform builder.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix, IResampler sampler)
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, ProjectiveTransformBuilder builder)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new ProjectiveTransformProcessor<TPixel>(matrix, sampler, source.GetCurrentSize()));
=> Transform(source, builder, KnownResamplers.Bicubic);
/// <summary>
/// Applies a projective transform to the image by the given matrix using the specified sampling algorithm.
/// TODO: Should we be offsetting the matrix here?
/// Performs a projective transform of an image using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="builder">The projective transform builder.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <param name="rectangle">The rectangle to constrain the transformed image to.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
internal static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix, IResampler sampler, Rectangle rectangle)
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, ProjectiveTransformBuilder builder, IResampler sampler)
where TPixel : struct, IPixel<TPixel>
{
var t = Matrix4x4.CreateTranslation(new Vector3(-rectangle.Location, 0));
Matrix4x4 combinedMatrix = t * matrix;
return source.ApplyProcessor(new ProjectiveTransformProcessor<TPixel>(combinedMatrix, sampler, rectangle.Size));
}
=> source.ApplyProcessor(new ProjectiveTransformProcessor<TPixel>(builder.BuildMatrix(), sampler, builder.Size));
}
}

2
tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs

@ -79,7 +79,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
using (Image<TPixel> image = provider.GetImage())
{
AffineTransformBuilder builder = new AffineTransformBuilder(image.Size())
.AppendRotateMatrixDegrees((float)Math.PI / 4F);
.AppendRotateMatrixDegrees(30);
image.Mutate(c => c.Transform(builder, resampler));
image.DebugSave(provider, resamplerName);

27
tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs

@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
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),
@ -60,10 +60,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
};
public ProjectiveTransformTests(ITestOutputHelper output)
{
this.Output = output;
}
public ProjectiveTransformTests(ITestOutputHelper output) => this.Output = output;
[Theory]
[WithTestPatternImages(nameof(ResamplerNames), 150, 150, PixelTypes.Rgba32)]
@ -73,9 +70,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
IResampler sampler = GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
Matrix4x4 m = ProjectiveTransformHelper.CreateTaperMatrix(image.Size(), TaperSide.Right, TaperCorner.Both, .5F);
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size())
.AppendTaperMatrix(TaperSide.Right, TaperCorner.Both, .5F);
image.Mutate(i => { i.Transform(m, sampler); });
image.Mutate(i => i.Transform(builder, sampler));
image.DebugSave(provider, resamplerName);
image.CompareToReferenceOutput(ValidatorComparer, provider, resamplerName);
@ -89,8 +87,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
using (Image<TPixel> image = provider.GetImage())
{
Matrix4x4 m = ProjectiveTransformHelper.CreateTaperMatrix(image.Size(), taperSide, taperCorner, .5F);
image.Mutate(i => { i.Transform(m); });
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size())
.AppendTaperMatrix(taperSide, taperCorner, .5F);
image.Mutate(i => i.Transform(builder));
FormattableString testOutputDetails = $"{taperSide}-{taperCorner}";
image.DebugSave(provider, testOutputDetails);
@ -110,10 +110,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
// 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;
Matrix4x4 matrix = Matrix4x4.Identity;
matrix.M13 = 0.01F;
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder(image.Size())
.AppendMatrix(matrix);
image.Mutate(i => { i.Transform(m); });
image.Mutate(i => i.Transform(builder));
image.DebugSave(provider);
image.CompareToReferenceOutput(TolerantComparer, provider);

2
tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs

@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelXDimension).DataType);
Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelYDimension).DataType);
TransformHelpers.UpdateDimensionalMetData(img);
TransformProcessorHelpers.UpdateDimensionalMetData(img);
Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelXDimension).DataType);
Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelYDimension).DataType);

Loading…
Cancel
Save