From 73939baad20ac91ef379e58f5deb9957ebad2e06 Mon Sep 17 00:00:00 2001 From: James South Date: Wed, 13 Apr 2016 00:21:09 +1000 Subject: [PATCH] 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 --- .../Samplers/ImageSamplerExtensions.cs | 29 +--- src/ImageProcessorCore/Samplers/Resampler.cs | 164 +++++------------- .../Samplers/Resamplers/BoxResampler.cs | 7 +- src/ImageProcessorCore/Samplers/Resize.cs | 19 +- src/ImageProcessorCore/Samplers/Rotate.cs | 104 ++--------- .../Processors/Samplers/SamplerTests.cs | 17 +- 6 files changed, 69 insertions(+), 271 deletions(-) diff --git a/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs b/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs index 542050ecd..d741ccfd3 100644 --- a/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs +++ b/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs @@ -176,34 +176,7 @@ namespace ImageProcessorCore.Samplers /// The public static Image Rotate(this Image source, float degrees, ProgressEventHandler progressHandler = null) { - return Rotate(source, degrees, new BicubicResampler(), false, progressHandler); - } - - /// - /// Rotates an image by the given angle in degrees. - /// - /// The image to resize. - /// The angle in degrees to perform the rotation. - /// Whether to compress and expand the image color-space to gamma correct the image during processing. - /// A delegate which is called as progress is made processing the image. - /// The - public static Image Rotate(this Image source, float degrees, bool compand, ProgressEventHandler progressHandler = null) - { - return Rotate(source, degrees, new BicubicResampler(), compand, progressHandler); - } - - /// - /// Rotates an image by the given angle in degrees. - /// - /// The image to resize. - /// The angle in degrees to perform the rotation. - /// The to perform the resampling. - /// Whether to compress and expand the image color-space to gamma correct the image during processing. - /// A delegate which is called as progress is made processing the image. - /// The - 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 diff --git a/src/ImageProcessorCore/Samplers/Resampler.cs b/src/ImageProcessorCore/Samplers/Resampler.cs index 0554b18e0..368e03193 100644 --- a/src/ImageProcessorCore/Samplers/Resampler.cs +++ b/src/ImageProcessorCore/Samplers/Resampler.cs @@ -6,8 +6,6 @@ namespace ImageProcessorCore.Samplers { using System; - using System.Collections.Generic; - using System.Threading.Tasks; /// /// Provides methods that allow the resampling of images using various algorithms. @@ -54,58 +52,57 @@ namespace ImageProcessorCore.Samplers /// 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 builder = new List(); + + 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 builder = new List(); 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 builder = new List(); - // 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; - //} - /// /// Represents the weight to be added to a scaled pixel. /// - protected class Weight + protected struct Weight { - /// - /// The pixel index. - /// - public readonly int Index; - /// /// Initializes a new instance of the class. /// @@ -241,9 +162,14 @@ namespace ImageProcessorCore.Samplers } /// - /// Gets or sets the result of the interpolation algorithm. + /// Gets the pixel index. + /// + public int Index { get; } + + /// + /// Gets the result of the interpolation algorithm. /// - public float Value { get; set; } + public float Value { get; } } /// @@ -262,4 +188,4 @@ namespace ImageProcessorCore.Samplers public float Sum { get; set; } } } -} +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Samplers/Resamplers/BoxResampler.cs b/src/ImageProcessorCore/Samplers/Resamplers/BoxResampler.cs index 6ef5c0e35..82c405114 100644 --- a/src/ImageProcessorCore/Samplers/Resamplers/BoxResampler.cs +++ b/src/ImageProcessorCore/Samplers/Resamplers/BoxResampler.cs @@ -17,12 +17,7 @@ namespace ImageProcessorCore.Samplers /// public float GetValue(float x) { - if (x < 0) - { - x = -x; - } - - if (x <= 0.5) + if (x > -0.5 && x <= 0.5) { return 1; } diff --git a/src/ImageProcessorCore/Samplers/Resize.cs b/src/ImageProcessorCore/Samplers/Resize.cs index dc6a92f5e..e7de40c44 100644 --- a/src/ImageProcessorCore/Samplers/Resize.cs +++ b/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(); } }); diff --git a/src/ImageProcessorCore/Samplers/Rotate.cs b/src/ImageProcessorCore/Samplers/Rotate.cs index 77cc8e7e7..42032ae2a 100644 --- a/src/ImageProcessorCore/Samplers/Rotate.cs +++ b/src/ImageProcessorCore/Samplers/Rotate.cs @@ -10,24 +10,13 @@ namespace ImageProcessorCore.Samplers /// /// Provides methods that allow the rotating of images using various algorithms. /// - public class Rotate : Resampler + public class Rotate : ImageSampler { /// /// The angle of rotation. /// private float angle; - /// - /// Initializes a new instance of the class. - /// - /// - /// The sampler to perform the resize operation. - /// - public Rotate(IResampler sampler) - : base(sampler) - { - } - /// /// Gets or sets the angle of rotation. /// @@ -54,16 +43,6 @@ namespace ImageProcessorCore.Samplers } } - /// - 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); - } - } - /// 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(); } }); diff --git a/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs b/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs index c419ae560..df1914c3e 100644 --- a/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs +++ b/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"); } } }