Browse Source

ParallelHelper -> ProjectiveTransformProcessor, better RotateTests

af/merge-core
Anton Firszov 7 years ago
parent
commit
7d415fbc27
  1. 216
      src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs
  2. 23
      tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs
  3. 2
      tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs
  4. 2
      tests/Images/External

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

@ -10,6 +10,7 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory; using SixLabors.Memory;
using SixLabors.Primitives; using SixLabors.Primitives;
@ -75,28 +76,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.Sampler is NearestNeighborResampler) if (this.Sampler is NearestNeighborResampler)
{ {
ParallelFor.WithConfiguration( ParallelHelper.IterateRows(
0, targetBounds,
height,
configuration, configuration,
y => rows =>
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{ {
var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
float z = MathF.Max(v3.Z, Epsilon); for (int x = 0; x < width; x++)
int px = (int)MathF.Round(v3.X / z); {
int py = (int)MathF.Round(v3.Y / z); var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix);
if (sourceBounds.Contains(px, py)) float z = MathF.Max(v3.Z, Epsilon);
{ int px = (int)MathF.Round(v3.X / z);
destRow[x] = source[px, py]; int py = (int)MathF.Round(v3.Y / z);
if (sourceBounds.Contains(px, py))
{
destRow[x] = source[px, py];
}
}
} }
} });
});
return; return;
} }
@ -121,92 +124,113 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height)) using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height))
using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height)) using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height))
{ {
ParallelFor.WithConfiguration( ParallelHelper.IterateRows(
0, targetBounds,
height,
configuration, configuration,
y => rows =>
{
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 for (int y = rows.Min; y < rows.Max; y++)
// 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;
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); ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y));
} ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y));
// Now multiply the results against the offsets for (int x = 0; x < width; x++)
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); // Use the single precision position to calculate correct bounding pixels
var vector = source[i, j].ToVector4(); // otherwise we get rogue pixels outside of the bounds.
var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix);
// Values are first premultiplied to prevent darkening of edge pixels float z = MathF.Max(v3.Z, Epsilon);
Vector4 multiplied = vector.Premultiply();
sum += multiplied * xWeight * yWeight; // 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;
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);
var vector = source[i, j].ToVector4();
// Values are first premultiplied to prevent darkening of edge pixels
Vector4 multiplied = vector.Premultiply();
sum += multiplied * xWeight * yWeight;
}
}
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
// Reverse the premultiplication
dest.PackFromVector4(sum.UnPremultiply());
} }
} }
});
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
// Reverse the premultiplication
dest.PackFromVector4(sum.UnPremultiply());
}
});
} }
} }

23
tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs

@ -7,7 +7,8 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{ {
public class RotateTests : FileTestBase [GroupOutput("Transforms")]
public class RotateTests
{ {
public static readonly TheoryData<float> RotateAngles public static readonly TheoryData<float> RotateAngles
= new TheoryData<float> = new TheoryData<float>
@ -25,29 +26,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
}; };
[Theory] [Theory]
[WithTestPatternImages(nameof(RotateAngles), 100, 50, DefaultPixelType)] [WithTestPatternImages(nameof(RotateAngles), 100, 50, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(RotateAngles), 50, 100, DefaultPixelType)] [WithTestPatternImages(nameof(RotateAngles), 50, 100, PixelTypes.Rgba32)]
public void Rotate_WithAngle<TPixel>(TestImageProvider<TPixel> provider, float value) public void Rotate_WithAngle<TPixel>(TestImageProvider<TPixel> provider, float value)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false);
{
image.Mutate(x => x.Rotate(value));
image.DebugSave(provider, value);
}
} }
[Theory] [Theory]
[WithTestPatternImages(nameof(RotateEnumValues), 100, 50, DefaultPixelType)] [WithTestPatternImages(nameof(RotateEnumValues), 100, 50, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(RotateEnumValues), 50, 100, DefaultPixelType)] [WithTestPatternImages(nameof(RotateEnumValues), 50, 100, PixelTypes.Rgba32)]
public void Rotate_WithRotateTypeEnum<TPixel>(TestImageProvider<TPixel> provider, RotateMode value) public void Rotate_WithRotateTypeEnum<TPixel>(TestImageProvider<TPixel> provider, RotateMode value)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) provider.RunValidatingProcessorTest(ctx => ctx.Rotate(value), value, appendPixelTypeToFileName: false);
{
image.Mutate(x => x.Rotate(value));
image.DebugSave(provider, value);
}
} }
} }
} }

2
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 static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.5f, 3);
private ITestOutputHelper Output { get; } private ITestOutputHelper Output { get; }
public static readonly TheoryData<string> ResamplerNames = new TheoryData<string> public static readonly TheoryData<string> ResamplerNames = new TheoryData<string>
{ {
nameof(KnownResamplers.Bicubic), nameof(KnownResamplers.Bicubic),

2
tests/Images/External

@ -1 +1 @@
Subproject commit c1e14c0e431066c57585f255d3feb8d3a1860d50 Subproject commit 7f4d2d64c6b820ca2b6827e6a8540a1013305ccf
Loading…
Cancel
Save