diff --git a/src/ImageProcessor/Samplers/ImageSampleExtensions.cs b/src/ImageProcessor/Samplers/ImageSampleExtensions.cs index 43f7e7dc8..30ae55c71 100644 --- a/src/ImageProcessor/Samplers/ImageSampleExtensions.cs +++ b/src/ImageProcessor/Samplers/ImageSampleExtensions.cs @@ -69,7 +69,7 @@ namespace ImageProcessor.Samplers /// Passing zero for one of height or width will automatically preserve the aspect ratio of the original image public static Image Resize(this Image source, int width, int height) { - return Resize(source, width, height, new RobidouxResampler()); + return Resize(source, width, height, new BicubicResampler()); } /// @@ -105,12 +105,13 @@ namespace ImageProcessor.Samplers { width = source.Width * height / source.Height; } + if (height == 0 && width > 0) { height = source.Height * width / source.Width; } - return source.Process(width, height, sourceRectangle, new Rectangle(0, 0, width, height), new Resampler(sampler)); + return source.Process(width, height, sourceRectangle, new Rectangle(0, 0, width, height), new Resize(sampler)); } /// @@ -121,7 +122,7 @@ namespace ImageProcessor.Samplers /// The public static Image Rotate(this Image source, float degrees) { - return source.Process(source.Width, source.Height, source.Bounds, source.Bounds, new Resampler(new BicubicResampler()) { Angle = degrees }); + return Rotate(source, degrees, new BicubicResampler()); } /// @@ -133,7 +134,7 @@ namespace ImageProcessor.Samplers /// The public static Image Rotate(this Image source, float degrees, IResampler sampler) { - return source.Process(source.Width, source.Height, source.Bounds, source.Bounds, new Resampler(sampler) { Angle = degrees }); + return source.Process(source.Width, source.Height, source.Bounds, source.Bounds, new Rotate(sampler) { Angle = degrees }); } /// diff --git a/src/ImageProcessor/Samplers/Resampler.cs b/src/ImageProcessor/Samplers/Resampler.cs index 4fd105f16..6c00a604d 100644 --- a/src/ImageProcessor/Samplers/Resampler.cs +++ b/src/ImageProcessor/Samplers/Resampler.cs @@ -12,30 +12,15 @@ namespace ImageProcessor.Samplers /// /// Provides methods that allow the resampling of images using various algorithms. /// - public class Resampler : ParallelImageProcessor + public abstract class Resampler : ParallelImageProcessor { - /// - /// The angle of rotation. - /// - private float angle; - - /// - /// The horizontal weights. - /// - private Weights[] horizontalWeights; - - /// - /// The vertical weights. - /// - private Weights[] verticalWeights; - /// /// Initializes a new instance of the class. /// /// /// The sampler to perform the resize operation. /// - public Resampler(IResampler sampler) + protected Resampler(IResampler sampler) { Guard.NotNull(sampler, nameof(sampler)); @@ -48,272 +33,14 @@ namespace ImageProcessor.Samplers public IResampler Sampler { get; } /// - /// Gets or sets the angle of rotation. - /// - public float Angle - { - get - { - return this.angle; - } - - set - { - if (value > 360) - { - value -= 360; - } - - if (value < 0) - { - value += 360; - } - - this.angle = value; - } - } - - /// - 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) - { - bool rotate = this.angle > 0 && this.angle < 360; - - // Split the two methods up so we can keep standard resize as performant as possible. - if (rotate) - { - this.ApplyResizeAndRotate(target, source, targetRectangle, sourceRectangle, startY, endY); - } - else - { - this.ApplyResizeOnly(target, source, targetRectangle, startY, endY); - } - } - - /// - protected override void AfterApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle) - { - // Copy the pixels over. - if (source.Bounds == target.Bounds && Math.Abs(this.angle) < 0.001f) - { - target.ClonePixels(target.Width, target.Height, source.Pixels); - } - } - - /// - /// Resamples the specified at the specified location - /// and with the specified size. + /// Gets or sets the horizontal weights. /// - /// Target image to apply the process to. - /// The source image. Cannot be null. - /// - /// The structure that specifies the location and size of the drawn image. - /// The image is scaled to fit the rectangle. - /// - /// The index of the row within the source image to start processing. - /// The index of the row within the source image to end processing. - /// - /// The method keeps the source image unchanged and returns the - /// the result of image process as new image. - /// - private void ApplyResizeOnly(ImageBase target, ImageBase source, Rectangle targetRectangle, int startY, int endY) - { - // Jump out, we'll deal with that later. - if (source.Bounds == target.Bounds) - { - return; - } - - int targetY = targetRectangle.Y; - int targetBottom = targetRectangle.Bottom; - int startX = targetRectangle.X; - int endX = targetRectangle.Right; - - 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); - - for (int x = startX; x < endX; x++) - { - // X coordinates of source points - int originX = (int)((x - startX) * widthFactor); - - target[x, y] = source[originX, originY]; - } - } - }); - - // Break out now. - return; - } - - // Interpolate the image using the calculated weights. - // TODO: Figure out a way to split this up so we can reduce complexity and speed things up. - Parallel.For( - startY, - endY, - y => - { - if (y >= targetY && y < targetBottom) - { - Weight[] verticalValues = this.verticalWeights[y].Values; - - for (int x = startX; x < endX; x++) - { - Weight[] horizontalValues = this.horizontalWeights[x].Values; - - // Destination color components - Color destination = new Color(); - - foreach (Weight yw in verticalValues) - { - int originY = yw.Index; - - foreach (Weight xw in horizontalValues) - { - int originX = xw.Index; - Color sourceColor = Color.Expand(source[originX, originY]); - destination += sourceColor * yw.Value * xw.Value; - } - } - - destination = Color.Compress(destination); - target[x, y] = destination; - } - } - }); - } + protected Weights[] HorizontalWeights { get; set; } /// - /// Resamples and rotates the specified at the specified location - /// and with the specified size. + /// Gets or sets the vertical weights. /// - /// Target image to apply the process to. - /// The source image. Cannot be null. - /// - /// The structure that specifies the location and size of the drawn image. - /// The image is scaled to fit the rectangle. - /// - /// - /// The structure that specifies the portion of the image object to draw. - /// - /// The index of the row within the source image to start processing. - /// The index of the row within the source image to end processing. - /// - /// The method keeps the source image unchanged and returns the - /// the result of image process as new image. - /// - private void ApplyResizeAndRotate(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY) - { - int targetY = targetRectangle.Y; - int targetBottom = targetRectangle.Bottom; - int startX = targetRectangle.X; - int endX = targetRectangle.Right; - float negativeAngle = -this.angle; - Point centre = Rectangle.Center(sourceRectangle); - - 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); - - 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]; - } - } - } - }); - - // Break out now. - return; - } - - // Interpolate the image using the calculated weights. - Parallel.For( - startY, - endY, - y => - { - if (y >= targetY && y < targetBottom) - { - Weight[] verticalValues = this.verticalWeights[y].Values; - - for (int x = startX; x < endX; x++) - { - Weight[] horizontalValues = this.horizontalWeights[x].Values; - - // Destination color components - Color destination = new Color(); - - foreach (Weight yw in verticalValues) - { - 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 = Color.Expand(source[rotated.X, rotated.Y]); - destination += sourceColor * yw.Value * xw.Value; - } - } - } - - destination = Color.Compress(destination); - target[x, y] = destination; - } - } - }); - } + protected Weights[] VerticalWeights { get; set; } /// /// Computes the weights to apply at each pixel when resizing. @@ -323,7 +50,7 @@ namespace ImageProcessor.Samplers /// /// The . /// - private Weights[] PrecomputeWeights(int destinationSize, int sourceSize) + protected Weights[] PrecomputeWeights(int destinationSize, int sourceSize) { IResampler sampler = this.Sampler; float ratio = sourceSize / (float)destinationSize; @@ -344,51 +71,51 @@ namespace ImageProcessor.Samplers 0, destinationSize, i => + { + float center = ((i + .5f) * ratio) - 0.5f; + int start = (int)Math.Ceiling(center - scaledRadius); + + if (start < 0) { - float center = ((i + .5f) * ratio) - 0.5f; - int start = (int)Math.Ceiling(center - scaledRadius); + start = 0; + } - if (start < 0) - { - start = 0; - } + int end = (int)Math.Floor(center + scaledRadius); - int end = (int)Math.Floor(center + scaledRadius); + if (end > sourceSize) + { + end = sourceSize; - if (end > sourceSize) + if (end < start) { - end = sourceSize; - - if (end < start) - { - 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); + float sum = 0; + result[i] = new Weights(); - if (w < 0 || w > 0) - { - sum += w; - builder.Add(new Weight(a, w)); - } - } + List builder = new List(); + for (int a = start; a < end; a++) + { + float w = sampler.GetValue((a - center) / scale); - // Normalise the values - if (sum > 0 || sum < 0) + if (w < 0 || w > 0) { - builder.ForEach(w => w.Value /= sum); + sum += w; + builder.Add(new Weight(a, w)); } + } - result[i].Values = builder.ToArray(); - result[i].Sum = sum; - }); + // 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; } @@ -436,4 +163,4 @@ namespace ImageProcessor.Samplers public float Sum { get; set; } } } -} \ No newline at end of file +} diff --git a/src/ImageProcessor/Samplers/Resamplers/BoxResampler.cs b/src/ImageProcessor/Samplers/Resamplers/BoxResampler.cs index 71aca2eeb..37106a8ff 100644 --- a/src/ImageProcessor/Samplers/Resamplers/BoxResampler.cs +++ b/src/ImageProcessor/Samplers/Resamplers/BoxResampler.cs @@ -12,7 +12,7 @@ namespace ImageProcessor.Samplers public class BoxResampler : IResampler { /// - public float Radius => 0.5f; + public float Radius => 0.5F; /// public float GetValue(float x) diff --git a/src/ImageProcessor/Samplers/Resamplers/RobidouxResampler.cs b/src/ImageProcessor/Samplers/Resamplers/RobidouxResampler.cs index 253f8b723..4331d6cc8 100644 --- a/src/ImageProcessor/Samplers/Resamplers/RobidouxResampler.cs +++ b/src/ImageProcessor/Samplers/Resamplers/RobidouxResampler.cs @@ -17,8 +17,8 @@ namespace ImageProcessor.Samplers /// public float GetValue(float x) { - const float B = 0.3782f; - const float C = 0.3109f; + const float B = 0.3782158F; + const float C = 0.3108921F; return ImageMaths.GetBcValue(x, B, C); } diff --git a/src/ImageProcessor/Samplers/Resamplers/RobidouxSharpResampler.cs b/src/ImageProcessor/Samplers/Resamplers/RobidouxSharpResampler.cs index 14db96d8e..e4b3c09a4 100644 --- a/src/ImageProcessor/Samplers/Resamplers/RobidouxSharpResampler.cs +++ b/src/ImageProcessor/Samplers/Resamplers/RobidouxSharpResampler.cs @@ -17,8 +17,8 @@ namespace ImageProcessor.Samplers /// public float GetValue(float x) { - const float B = 0.2620f; - const float C = 0.3690f; + const float B = 0.26201451F; + const float C = 0.36899274F; return ImageMaths.GetBcValue(x, B, C); } diff --git a/src/ImageProcessor/Samplers/Resize.cs b/src/ImageProcessor/Samplers/Resize.cs new file mode 100644 index 000000000..b7eea3c7b --- /dev/null +++ b/src/ImageProcessor/Samplers/Resize.cs @@ -0,0 +1,158 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessor.Samplers +{ + using System.Threading.Tasks; + + /// + /// Provides methods that allow the resizing of images using various algorithms. + /// + public class Resize : Resampler + { + /// + /// The image used for storing the first pass pixels. + /// + private Image firstPass; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The sampler to perform the resize operation. + /// + public Resize(IResampler sampler) + : base(sampler) + { + } + + /// + public override int Parallelism { get; set; } = 1; + + /// + 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); + } + + this.firstPass = new Image(target.Width, source.Height); + } + + /// + protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY) + { + // Jump out, we'll deal with that later. + if (source.Bounds == target.Bounds) + { + return; + } + + int sourceBottom = source.Bounds.Bottom; + int targetY = targetRectangle.Y; + int targetBottom = targetRectangle.Bottom; + int startX = targetRectangle.X; + int endX = targetRectangle.Right; + + 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); + + for (int x = startX; x < endX; x++) + { + // X coordinates of source points + int originX = (int)((x - startX) * widthFactor); + + target[x, y] = source[originX, originY]; + } + } + }); + + // Break out now. + return; + } + + // Interpolate the image using the calculated weights. + // A 2-pass 1D algorithm appears to be faster than splitting a 1-pass 2D algorithm + // First process the columns. + Parallel.For( + 0, + sourceBottom, + y => + { + for (int x = startX; x < endX; x++) + { + Weight[] horizontalValues = this.HorizontalWeights[x].Values; + + // Destination color components + Color destination = new Color(); + + foreach (Weight xw in horizontalValues) + { + int originX = xw.Index; + Color sourceColor = Color.Expand(source[originX, y]); + destination += sourceColor * xw.Value; + } + + destination = Color.Compress(destination); + this.firstPass[x, y] = destination; + } + }); + + // Now process the rows. + Parallel.For( + startY, + endY, + y => + { + if (y >= targetY && y < targetBottom) + { + Weight[] verticalValues = this.VerticalWeights[y].Values; + + for (int x = startX; x < endX; x++) + { + // Destination color components + Color destination = new Color(); + + foreach (Weight yw in verticalValues) + { + int originY = yw.Index; + int originX = x; + Color sourceColor = Color.Expand(this.firstPass[originX, originY]); + destination += sourceColor * yw.Value; + } + + destination = Color.Compress(destination); + target[x, y] = destination; + } + } + }); + } + + /// + protected override void AfterApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle) + { + // Copy the pixels over. + if (source.Bounds == target.Bounds) + { + target.ClonePixels(target.Width, target.Height, source.Pixels); + } + } + } +} \ No newline at end of file diff --git a/src/ImageProcessor/Samplers/Rotate.cs b/src/ImageProcessor/Samplers/Rotate.cs new file mode 100644 index 000000000..71eb174c1 --- /dev/null +++ b/src/ImageProcessor/Samplers/Rotate.cs @@ -0,0 +1,159 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessor.Samplers +{ + using System.Threading.Tasks; + + /// + /// Provides methods that allow the rotating of images using various algorithms. + /// + public class Rotate : Resampler + { + /// + /// 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. + /// + public float Angle + { + get + { + return this.angle; + } + + set + { + if (value > 360) + { + value -= 360; + } + + if (value < 0) + { + value += 360; + } + + this.angle = value; + } + } + + /// + 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) + { + int targetY = targetRectangle.Y; + int targetBottom = targetRectangle.Bottom; + int startX = targetRectangle.X; + int endX = targetRectangle.Right; + float negativeAngle = -this.angle; + Point centre = Rectangle.Center(sourceRectangle); + + 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); + + 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]; + } + } + } + }); + + // Break out now. + return; + } + + // Interpolate the image using the calculated weights. + Parallel.For( + startY, + endY, + y => + { + if (y >= targetY && y < targetBottom) + { + Weight[] verticalValues = this.VerticalWeights[y].Values; + + for (int x = startX; x < endX; x++) + { + Weight[] horizontalValues = this.HorizontalWeights[x].Values; + + // Destination color components + Color destination = new Color(); + + foreach (Weight yw in verticalValues) + { + 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 = Color.Expand(source[rotated.X, rotated.Y]); + destination += sourceColor * yw.Value * xw.Value; + } + } + } + + destination = Color.Compress(destination); + target[x, y] = destination; + } + } + }); + } + } +} \ No newline at end of file