Browse Source

WIP make this work with scaled-down transforms.

If we normalize the weights to make this work  when the output is scaled down we break the edge pixel output. Somehow fix this.
af/merge-core
James Jackson-South 9 years ago
parent
commit
36c3a4b9f2
  1. 125
      src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs
  2. 47
      src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs
  3. 39
      src/ImageSharp/Processing/Transforms/Transform.cs
  4. 79
      tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs

125
src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs

@ -8,6 +8,7 @@ using System.Numerics;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
@ -26,7 +27,7 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// <summary>
/// Initializes a new instance of the <see cref="AffineProcessor{TPixel}"/> class.
/// </summary>
/// <param name="sampler">The sampler to perform the resize operation.</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
protected AffineProcessor(IResampler sampler)
{
this.Sampler = sampler;
@ -91,13 +92,15 @@ namespace SixLabors.ImageSharp.Processing.Processors
int maxSourceX = source.Width - 1;
int maxSourceY = source.Height - 1;
(float radius, float scale) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width);
(float radius, float scale) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height);
(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;
var radius = new Vector2(xRadiusScale.radius, yRadiusScale.radius);
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);
Parallel.For(
0,
@ -106,50 +109,90 @@ namespace SixLabors.ImageSharp.Processing.Processors
y =>
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
using (var yBuffer = new Buffer<float>(yLength))
using (var xBuffer = new Buffer<float>(xLength))
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var point = Vector2.Transform(new Vector2(x, y), matrix);
// Clamp sampling pixel radial extents to the source image edges
Vector2 maxXY = point + radius;
Vector2 minXY = point - radius;
var extents = new Vector4(
MathF.Ceiling(maxXY.X),
MathF.Ceiling(maxXY.Y),
MathF.Floor(minXY.X),
MathF.Floor(minXY.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 point = Vector2.Transform(new Vector2(x, y), matrix);
// Clamp sampling pixel radial extents to the source image edges
Vector2 maxXY = point + radius;
Vector2 minXY = point - radius;
var extents = new Vector4(
MathF.Ceiling(maxXY.X),
MathF.Ceiling(maxXY.Y),
MathF.Floor(minXY.X),
MathF.Floor(minXY.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;
// TODO: Find a way to speed this up if we can we precalculated weights!!!
// It appears these have to be calculated on-the-fly.
// Check with Anton to figure out why indexing from the precalculated weights was wrong.
//
// Create and normalize the y-weights
float ySum = 0;
for (int yy = 0, i = minY; i <= maxY; i++, yy++)
{
float weight = sampler.GetValue((i - point.Y) / yScale);
ySum += weight;
yBuffer[yy] = weight;
}
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
// TODO:
// Normalizing the weights fixes scaled transfrom where we scale down but breaks edge pixel belnding
// We end up with too much weight on pixels that should be blended.
// We could, maybe, move the division into the loop and not divide when we hit 0 or maxN but that seems clunky.
if (ySum > 0)
{
for (int i = 0; i < yBuffer.Length; i++)
{
yBuffer[i] = yBuffer[i] / ySum;
}
}
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
// Create and normalize the x-weights
float xSum = 0;
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{
float weight = sampler.GetValue((i - point.X) / xScale);
xSum += weight;
xBuffer[xx] = weight;
}
if (minX == maxX || minY == maxY)
{
continue;
}
if (xSum > 0)
{
for (int i = 0; i < xBuffer.Length; i++)
{
xBuffer[i] = xBuffer[i] / xSum;
}
}
// It appears these have to be calculated on-the-fly.
// Using the precalculated weights give the wrong values.
// TODO: Find a way to speed this up if we can.
Vector4 sum = Vector4.Zero;
for (int i = minX; i <= maxX; i++)
{
float weightX = sampler.GetValue((i - point.X) / xScale);
for (int j = minY; j <= maxY; j++)
// Now multiply the normalized results against the offsets
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float weightY = sampler.GetValue((j - point.Y) / yScale);
sum += source[i, j].ToVector4() * weightX * weightY;
float yWeight = yBuffer[yy];
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{
float xWeight = xBuffer[xx];
sum += source[i, j].ToVector4() * xWeight * yWeight;
}
}
}
ref TPixel dest = ref destRow[x];
dest.PackFromVector4(sum);
ref TPixel dest = ref destRow[x];
dest.PackFromVector4(sum);
}
}
});
}
@ -186,7 +229,7 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// <param name="sourceSize">The source dimension size</param>
/// <param name="destinationSize">The destination dimension size</param>
/// <returns>The radius, and scaling factor</returns>
private (float radius, float scale) GetSamplingRadius(int sourceSize, int destinationSize)
private (float radius, float scale, float ratio) GetSamplingRadius(int sourceSize, int destinationSize)
{
float ratio = (float)sourceSize / destinationSize;
float scale = ratio;
@ -196,7 +239,7 @@ namespace SixLabors.ImageSharp.Processing.Processors
scale = 1F;
}
return (MathF.Ceiling(scale * this.Sampler.Radius), scale);
return (MathF.Ceiling(scale * this.Sampler.Radius), scale, ratio);
}
}
}

47
src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs

@ -0,0 +1,47 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors
{
/// <summary>
/// Provides methods that allow the tranformation of images using various algorithms.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class TransformProcessor<TPixel> : AffineProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="TransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transformation matrix</param>
public TransformProcessor(Matrix3x2 matrix)
: this(matrix, KnownResamplers.NearestNeighbor)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transformation matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
public TransformProcessor(Matrix3x2 matrix, IResampler sampler)
: base(sampler)
{
this.TransformMatrix = matrix;
}
/// <summary>
/// Gets the transform matrix
/// </summary>
public Matrix3x2 TransformMatrix { get; }
/// <inheritdoc />
protected override Matrix3x2 GetTransformMatrix()
{
return this.TransformMatrix;
}
}
}

39
src/ImageSharp/Processing/Transforms/Transform.cs

@ -0,0 +1,39 @@
// 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;
using SixLabors.ImageSharp.Processing.Processors;
namespace SixLabors.ImageSharp
{
/// <summary>
/// Extension methods for the <see cref="Image{TPixel}"/> type.
/// </summary>
public static partial class ImageExtensions
{
/// <summary>
/// Transforms an image by the given matrix.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to skew.</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, Matrix3x2 matrix)
where TPixel : struct, IPixel<TPixel>
=> Transform(source, matrix, KnownResamplers.NearestNeighbor);
/// <summary>
/// Transforms an image by the given matrix using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to skew.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix3x2 matrix, IResampler sampler)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new TransformProcessor<TPixel>(matrix, sampler));
}
}

79
tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
using System.Numerics;
using System.Reflection;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.Primitives;
using Xunit;
public class TransformTests : FileTestBase
{
public static readonly TheoryData<float, float, float> TransformValues
= new TheoryData<float, float, float>
{
{ 20, 10, 50 },
{ -20, -10, 50 }
};
public static readonly List<string> ResamplerNames
= new List<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),
};
[Theory]
[WithFileCollection(nameof(DefaultFiles), nameof(TransformValues), DefaultPixelType)]
public void ImageShouldTransformWithSampler<TPixel>(TestImageProvider<TPixel> provider, float x, float y, float z)
where TPixel : struct, IPixel<TPixel>
{
foreach (string resamplerName in ResamplerNames)
{
IResampler sampler = GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(-z);
// TODO, how does scale work? 2 means half just now,
Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(2F, 2F));
image.Mutate(i => i.Transform(scale * rotate, sampler));
image.DebugSave(provider, string.Join("_", x, y, resamplerName));
}
}
}
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);
}
}
}
Loading…
Cancel
Save