Browse Source

Improve Resize and Rotate

Resize is more accurate + should be faster for larger images.
Rotate is now faster.


Former-commit-id: 3048d78e60fe62e826fe0b6fcb13a83ed6cf38eb
Former-commit-id: dc9eacc4bc7a9c9c33c155bdaa871b15b62816ff
Former-commit-id: dd17b2b62035f8bf3af68167e8e2cb72ac2dfcb3
af/merge-core
James South 10 years ago
parent
commit
73939baad2
  1. 29
      src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs
  2. 164
      src/ImageProcessorCore/Samplers/Resampler.cs
  3. 7
      src/ImageProcessorCore/Samplers/Resamplers/BoxResampler.cs
  4. 19
      src/ImageProcessorCore/Samplers/Resize.cs
  5. 104
      src/ImageProcessorCore/Samplers/Rotate.cs
  6. 17
      tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs

29
src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs

@ -176,34 +176,7 @@ namespace ImageProcessorCore.Samplers
/// <returns>The <see cref="Image"/></returns>
public static Image Rotate(this Image source, float degrees, ProgressEventHandler progressHandler = null)
{
return Rotate(source, degrees, new BicubicResampler(), false, progressHandler);
}
/// <summary>
/// Rotates an image by the given angle in degrees.
/// </summary>
/// <param name="source">The image to resize.</param>
/// <param name="degrees">The angle in degrees to perform the rotation.</param>
/// <param name="compand">Whether to compress and expand the image color-space to gamma correct the image during processing.</param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image"/></returns>
public static Image Rotate(this Image source, float degrees, bool compand, ProgressEventHandler progressHandler = null)
{
return Rotate(source, degrees, new BicubicResampler(), compand, progressHandler);
}
/// <summary>
/// Rotates an image by the given angle in degrees.
/// </summary>
/// <param name="source">The image to resize.</param>
/// <param name="degrees">The angle in degrees to perform the rotation.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <param name="compand">Whether to compress and expand the image color-space to gamma correct the image during processing.</param>
/// <param name="progressHandler">A delegate which is called as progress is made processing the image.</param>
/// <returns>The <see cref="Image"/></returns>
public static Image Rotate(this Image source, float degrees, IResampler sampler, bool compand, ProgressEventHandler progressHandler = null)
{
Rotate processor = new Rotate(sampler) { Angle = degrees, Compand = compand };
Rotate processor = new Rotate { Angle = degrees };
processor.OnProgress += progressHandler;
try

164
src/ImageProcessorCore/Samplers/Resampler.cs

@ -6,8 +6,6 @@
namespace ImageProcessorCore.Samplers
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
/// <summary>
/// Provides methods that allow the resampling of images using various algorithms.
@ -54,58 +52,57 @@ namespace ImageProcessorCore.Samplers
/// </returns>
protected Weights[] PrecomputeWeights(int destinationSize, int sourceSize)
{
float xscale = destinationSize / (float)sourceSize;
float width;
float scale = (float)destinationSize / sourceSize;
IResampler sampler = this.Sampler;
float fwidth = sampler.Radius;
float fscale;
float radius = sampler.Radius;
double left;
double right;
double weight = 0;
int n = 0;
int k;
double weight;
int index;
int sum;
Weights[] result = new Weights[destinationSize];
// When expanding, broaden the effective kernel support so that we still
// When shrinking, broaden the effective kernel support so that we still
// visit every source pixel.
if (xscale < 0)
if (scale < 1)
{
width = sampler.Radius / xscale;
fscale = 1 / xscale;
float width = radius / scale;
float filterScale = 1 / scale;
// Make the weights slices, one source for each column or row.
for (int i = 0; i < destinationSize; i++)
{
float centre = i / xscale;
float centre = i / scale;
left = Math.Ceiling(centre - width);
right = Math.Floor(centre + width);
float sum = 0;
result[i] = new Weights();
List<Weight> builder = new List<Weight>();
result[i] = new Weights
{
Sum = 0,
Values = new Weight[(int)Math.Floor(2 * width + 1)]
};
for (double j = left; j <= right; j++)
{
weight = centre - j;
weight = sampler.GetValue((float)weight / fscale) / fscale;
weight = sampler.GetValue((float)(weight / filterScale)) / filterScale;
if (j < 0)
{
n = (int)-j;
index = (int)-j;
}
else if (j >= sourceSize)
{
n = (int)((sourceSize - j) + sourceSize - 1);
index = (int)((sourceSize - j) + sourceSize - 1);
}
else
{
n = (int)j;
index = (int)j;
}
sum++;
builder.Add(new Weight(n, (float)weight));
sum = (int)result[i].Sum++;
result[i].Values[sum] = new Weight(index, (float)weight);
}
result[i].Values = builder.ToArray();
result[i].Sum = sum;
}
}
else
@ -113,122 +110,46 @@ namespace ImageProcessorCore.Samplers
// Make the weights slices, one source for each column or row.
for (int i = 0; i < destinationSize; i++)
{
float centre = i / xscale;
left = Math.Ceiling(centre - fwidth);
right = Math.Floor(centre + fwidth);
float sum = 0;
result[i] = new Weights();
float centre = i / scale;
left = Math.Ceiling(centre - radius);
right = Math.Floor(centre + radius);
result[i] = new Weights
{
Sum = 0,
Values = new Weight[(int)(radius * 2 + 1)]
};
List<Weight> builder = new List<Weight>();
for (double j = left; j <= right; j++)
{
weight = centre - j;
weight = sampler.GetValue((float)weight);
if (j < 0)
{
n = (int)-j;
index = (int)-j;
}
else if (j >= sourceSize)
{
n = (int)((sourceSize - j) + sourceSize - 1);
index = (int)((sourceSize - j) + sourceSize - 1);
}
else
{
n = (int)j;
index = (int)j;
}
sum++;
builder.Add(new Weight(n, (float)weight));
sum = (int)result[i].Sum++;
result[i].Values[sum] = new Weight(index, (float)weight);
}
result[i].Values = builder.ToArray();
result[i].Sum = sum;
}
}
return result;
}
//protected Weights[] PrecomputeWeights(int destinationSize, int sourceSize)
//{
// IResampler sampler = this.Sampler;
// float ratio = sourceSize / (float)destinationSize;
// float scale = ratio;
// // When shrinking, broaden the effective kernel support so that we still
// // visit every source pixel.
// if (scale < 1)
// {
// scale = 1;
// }
// float scaledRadius = (float)Math.Ceiling(scale * sampler.Radius);
// Weights[] result = new Weights[destinationSize];
// // Make the weights slices, one source for each column or row.
// Parallel.For(
// 0,
// destinationSize,
// i =>
// {
// float center = ((i + .5f) * ratio) - 0.5f;
// int start = (int)Math.Ceiling(center - scaledRadius);
// if (start < 0)
// {
// start = 0;
// }
// int end = (int)Math.Floor(center + scaledRadius);
// if (end > sourceSize)
// {
// end = sourceSize;
// if (end < start)
// {
// end = start;
// }
// }
// float sum = 0;
// result[i] = new Weights();
// List<Weight> builder = new List<Weight>();
// for (int a = start; a < end; a++)
// {
// float w = sampler.GetValue((a - center) / scale);
// if (w < 0 || w > 0)
// {
// sum += w;
// builder.Add(new Weight(a, w));
// }
// }
// // Normalise the values
// if (sum > 0 || sum < 0)
// {
// builder.ForEach(w => w.Value /= sum);
// }
// result[i].Values = builder.ToArray();
// result[i].Sum = sum;
// });
// return result;
//}
/// <summary>
/// Represents the weight to be added to a scaled pixel.
/// </summary>
protected class Weight
protected struct Weight
{
/// <summary>
/// The pixel index.
/// </summary>
public readonly int Index;
/// <summary>
/// Initializes a new instance of the <see cref="Weight"/> class.
/// </summary>
@ -241,9 +162,14 @@ namespace ImageProcessorCore.Samplers
}
/// <summary>
/// Gets or sets the result of the interpolation algorithm.
/// Gets the pixel index.
/// </summary>
public int Index { get; }
/// <summary>
/// Gets the result of the interpolation algorithm.
/// </summary>
public float Value { get; set; }
public float Value { get; }
}
/// <summary>
@ -262,4 +188,4 @@ namespace ImageProcessorCore.Samplers
public float Sum { get; set; }
}
}
}
}

7
src/ImageProcessorCore/Samplers/Resamplers/BoxResampler.cs

@ -17,12 +17,7 @@ namespace ImageProcessorCore.Samplers
/// <inheritdoc/>
public float GetValue(float x)
{
if (x < 0)
{
x = -x;
}
if (x <= 0.5)
if (x > -0.5 && x <= 0.5)
{
return 1;
}

19
src/ImageProcessorCore/Samplers/Resize.cs

@ -114,13 +114,6 @@ namespace ImageProcessorCore.Samplers
destination += sourceColor * xw.Value;
}
//foreach (Weight xw in horizontalValues)
//{
// int originX = xw.Index;
// Color sourceColor = compand ? Color.Expand(source[originX, y]) : source[originX, y];
// destination += sourceColor * xw.Value;
//}
if (compand)
{
destination = Color.Compress(destination);
@ -150,19 +143,10 @@ namespace ImageProcessorCore.Samplers
{
Weight yw = verticalValues[i];
int originY = yw.Index;
int originX = x;
Color sourceColor = compand ? Color.Expand(this.firstPass[originX, originY]) : this.firstPass[originX, originY];
Color sourceColor = compand ? Color.Expand(this.firstPass[x, originY]) : this.firstPass[x, originY];
destination += sourceColor * yw.Value;
}
//foreach (Weight yw in verticalValues)
//{
// int originY = yw.Index;
// int originX = x;
// Color sourceColor = compand ? Color.Expand(this.firstPass[originX, originY]) : this.firstPass[originX, originY];
// destination += sourceColor * yw.Value;
//}
if (compand)
{
destination = Color.Compress(destination);
@ -170,6 +154,7 @@ namespace ImageProcessorCore.Samplers
target[x, y] = destination;
}
this.OnRowProcessed();
}
});

104
src/ImageProcessorCore/Samplers/Rotate.cs

@ -10,24 +10,13 @@ namespace ImageProcessorCore.Samplers
/// <summary>
/// Provides methods that allow the rotating of images using various algorithms.
/// </summary>
public class Rotate : Resampler
public class Rotate : ImageSampler
{
/// <summary>
/// The angle of rotation.
/// </summary>
private float angle;
/// <summary>
/// Initializes a new instance of the <see cref="Rotate"/> class.
/// </summary>
/// <param name="sampler">
/// The sampler to perform the resize operation.
/// </param>
public Rotate(IResampler sampler)
: base(sampler)
{
}
/// <summary>
/// Gets or sets the angle of rotation.
/// </summary>
@ -54,16 +43,6 @@ namespace ImageProcessorCore.Samplers
}
}
/// <inheritdoc/>
protected override void OnApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle)
{
if (!(this.Sampler is NearestNeighborResampler))
{
this.HorizontalWeights = this.PrecomputeWeights(targetRectangle.Width, sourceRectangle.Width);
this.VerticalWeights = this.PrecomputeWeights(targetRectangle.Height, sourceRectangle.Height);
}
}
/// <inheritdoc/>
protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{
@ -73,45 +52,11 @@ namespace ImageProcessorCore.Samplers
int endX = targetRectangle.Right;
float negativeAngle = -this.angle;
Point centre = Rectangle.Center(sourceRectangle);
bool compand = this.Compand;
if (this.Sampler is NearestNeighborResampler)
{
// Scaling factors
float widthFactor = source.Width / (float)target.Width;
float heightFactor = source.Height / (float)target.Height;
Parallel.For(
startY,
endY,
y =>
{
if (y >= targetY && y < targetBottom)
{
// Y coordinates of source points
int originY = (int)((y - targetY) * heightFactor);
// Scaling factors
float widthFactor = source.Width / (float)target.Width;
float heightFactor = source.Height / (float)target.Height;
for (int x = startX; x < endX; x++)
{
// X coordinates of source points
int originX = (int)((x - startX) * widthFactor);
// Rotate at the centre point
Point rotated = Point.Rotate(new Point(originX, originY), centre, negativeAngle);
if (sourceRectangle.Contains(rotated.X, rotated.Y))
{
target[x, y] = source[rotated.X, rotated.Y];
}
}
this.OnRowProcessed();
}
});
// Break out now.
return;
}
// Interpolate the image using the calculated weights.
Parallel.For(
startY,
endY,
@ -119,46 +64,21 @@ namespace ImageProcessorCore.Samplers
{
if (y >= targetY && y < targetBottom)
{
Weight[] verticalValues = this.VerticalWeights[y].Values;
// Y coordinates of source points
int originY = (int)((y - targetY) * heightFactor);
for (int x = startX; x < endX; x++)
{
Weight[] horizontalValues = this.HorizontalWeights[x].Values;
// X coordinates of source points
int originX = (int)((x - startX) * widthFactor);
// Destination color components
Color destination = new Color();
foreach (Weight yw in verticalValues)
// Rotate at the centre point
Point rotated = Point.Rotate(new Point(originX, originY), centre, negativeAngle);
if (sourceRectangle.Contains(rotated.X, rotated.Y))
{
int originY = yw.Index;
foreach (Weight xw in horizontalValues)
{
int originX = xw.Index;
// Rotate at the centre point
Point rotated = Point.Rotate(new Point(originX, originY), centre, negativeAngle);
if (sourceRectangle.Contains(rotated.X, rotated.Y))
{
target[x, y] = source[rotated.X, rotated.Y];
}
if (sourceRectangle.Contains(rotated.X, rotated.Y))
{
Color sourceColor = compand ? Color.Expand(source[rotated.X, rotated.Y]) : source[rotated.X, rotated.Y];
destination += sourceColor * yw.Value * xw.Value;
}
}
target[x, y] = source[rotated.X, rotated.Y];
}
if (compand)
{
destination = Color.Compress(destination);
}
target[x, y] = destination;
}
this.OnRowProcessed();
}
});

17
tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs

@ -15,13 +15,13 @@
{
{ "Bicubic", new BicubicResampler() },
{ "Triangle", new TriangleResampler() },
{ "NearestNeighbor", new NearestNeighborResampler() },
// Perf: Enable for local testing only
//{ "Box", new BoxResampler() },
//{ "Lanczos3", new Lanczos3Resampler() },
//{ "Lanczos5", new Lanczos5Resampler() },
//{ "Lanczos8", new Lanczos8Resampler() },
//{ "MitchellNetravali", new MitchellNetravaliResampler() },
{ "NearestNeighbor", new NearestNeighborResampler() },
//{ "Hermite", new HermiteResampler() },
//{ "Spline", new SplineResampler() },
//{ "Robidoux", new RobidouxResampler() },
@ -92,7 +92,7 @@
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}"))
{
image.Resize(image.Width * 2, image.Height * 2, sampler, false, this.ProgressUpdate)
image.Resize(image.Width / 2, image.Height / 2, sampler, false, this.ProgressUpdate)
.Save(output);
}
@ -184,9 +184,8 @@
}
}
[Theory]
[MemberData("ReSamplers")]
public void ImageShouldRotate(string name, IResampler sampler)
[Fact]
public void ImageShouldRotate()
{
if (!Directory.Exists("TestOutput/Rotate"))
{
@ -199,15 +198,15 @@
{
Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream);
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
string filename = Path.GetFileName(file);
using (FileStream output = File.OpenWrite($"TestOutput/Rotate/{filename}"))
{
image.Rotate(45, sampler, false, this.ProgressUpdate)
//.BackgroundColor(Color.Aqua)
image.Rotate(45, this.ProgressUpdate)
.BackgroundColor(Color.Pink)
.Save(output);
}
Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms");
Trace.WriteLine($"{watch.ElapsedMilliseconds}ms");
}
}
}

Loading…
Cancel
Save