diff --git a/src/ImageProcessor/Samplers/ImageSampleExtensions.cs b/src/ImageProcessor/Samplers/ImageSampleExtensions.cs index 02b77e423..78ab22a52 100644 --- a/src/ImageProcessor/Samplers/ImageSampleExtensions.cs +++ b/src/ImageProcessor/Samplers/ImageSampleExtensions.cs @@ -105,11 +105,23 @@ namespace ImageProcessor.Samplers /// Rotates an image by the given angle in degrees. /// /// The image to resize. - /// The angle in degrees to porform the rotation. + /// The angle in degrees to perform the rotation. /// The public static Image Rotate(this Image source, float degrees) { return source.Process(source.Width, source.Height, source.Bounds, source.Bounds, new Resampler(new RobidouxResampler()) { Angle = degrees }); } + + /// + /// 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. + /// 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 }); + } } } diff --git a/src/ImageProcessor/Samplers/Resampler.cs b/src/ImageProcessor/Samplers/Resampler.cs index ba14fb3ea..60d22cae2 100644 --- a/src/ImageProcessor/Samplers/Resampler.cs +++ b/src/ImageProcessor/Samplers/Resampler.cs @@ -77,8 +77,11 @@ namespace ImageProcessor.Samplers /// protected override void OnApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle) { - this.horizontalWeights = this.PrecomputeWeights(targetRectangle.Width, sourceRectangle.Width); - this.verticalWeights = this.PrecomputeWeights(targetRectangle.Height, sourceRectangle.Height); + if (!(this.Sampler is NearestNeighborResampler)) + { + this.horizontalWeights = this.PrecomputeWeights(targetRectangle.Width, sourceRectangle.Width); + this.verticalWeights = this.PrecomputeWeights(targetRectangle.Height, sourceRectangle.Height); + } } /// @@ -126,6 +129,37 @@ namespace ImageProcessor.Samplers 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. Parallel.For( startY, endY, @@ -198,6 +232,45 @@ namespace ImageProcessor.Samplers float negativeAngle = -this.angle; Vector2 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 + Vector2 rotated = ImageMaths.RotatePoint(new Vector2(originX, originY), centre, negativeAngle); + int rotatedX = (int)rotated.X; + int rotatedY = (int)rotated.Y; + + if (sourceRectangle.Contains(rotatedX, rotatedY)) + { + target[x, y] = source[rotatedX, rotatedY]; + } + } + } + }); + + // Break out now. + return; + } + + // Interpolate the image using the calculated weights. Parallel.For( startY, endY, @@ -236,13 +309,6 @@ namespace ImageProcessor.Samplers destination.B += sourceColor.B * weight; destination.A += sourceColor.A * weight; } - else - { - // This is well hacky but clears up most of the - // Alpha bleeding issues present in rotated images. - float weight = yw.Value * xw.Value; - destination.A += .9f * weight; - } } } diff --git a/src/ImageProcessor/Samplers/Resamplers/BicubicResampler.cs b/src/ImageProcessor/Samplers/Resamplers/BicubicResampler.cs index 1f6cdff35..d5d9905fd 100644 --- a/src/ImageProcessor/Samplers/Resamplers/BicubicResampler.cs +++ b/src/ImageProcessor/Samplers/Resamplers/BicubicResampler.cs @@ -8,6 +8,7 @@ namespace ImageProcessor.Samplers /// /// The function implements the bicubic kernel algorithm W(x) as described on /// Wikipedia + /// A commonly used algorithm within imageprocessing that preserves sharpness better than triangle interpolation. /// public class BicubicResampler : IResampler { diff --git a/src/ImageProcessor/Samplers/Resamplers/BoxResampler.cs b/src/ImageProcessor/Samplers/Resamplers/BoxResampler.cs index 8020eca9d..71aca2eeb 100644 --- a/src/ImageProcessor/Samplers/Resamplers/BoxResampler.cs +++ b/src/ImageProcessor/Samplers/Resamplers/BoxResampler.cs @@ -6,7 +6,8 @@ namespace ImageProcessor.Samplers { /// - /// The function implements the box (nearest neighbour) algorithm. + /// The function implements the box algorithm. Similar to nearest neighbour when upscaling. + /// When downscaling the pixels will average, merging together. /// public class BoxResampler : IResampler { diff --git a/src/ImageProcessor/Samplers/Resamplers/NearestNeighborResampler.cs b/src/ImageProcessor/Samplers/Resamplers/NearestNeighborResampler.cs new file mode 100644 index 000000000..61f520a78 --- /dev/null +++ b/src/ImageProcessor/Samplers/Resamplers/NearestNeighborResampler.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessor.Samplers +{ + /// + /// The function implements the nearest neighbour algorithm. This uses an unscaled filter + /// which will select the closest pixel to the new pixels position. + /// + public class NearestNeighborResampler : IResampler + { + /// + public float Radius => 1; + + /// + public float GetValue(float x) + { + return x; + } + } +} diff --git a/src/ImageProcessor/Samplers/Resamplers/TriangleResampler.cs b/src/ImageProcessor/Samplers/Resamplers/TriangleResampler.cs index 85f3ea9f7..3b6c8bd97 100644 --- a/src/ImageProcessor/Samplers/Resamplers/TriangleResampler.cs +++ b/src/ImageProcessor/Samplers/Resamplers/TriangleResampler.cs @@ -7,6 +7,8 @@ namespace ImageProcessor.Samplers { /// /// The function implements the triangle (bilinear) algorithm. + /// Bilinear interpolation can be used where perfect image transformation with pixel matching is impossible, + /// so that one can calculate and assign appropriate intensity values to pixels. /// public class TriangleResampler : IResampler { diff --git a/tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs b/tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs index d91c8eb7c..7c73198ca 100644 --- a/tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs +++ b/tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs @@ -21,6 +21,7 @@ namespace ImageProcessor.Tests { "Lanczos5", new Lanczos5Resampler() }, { "Lanczos8", new Lanczos8Resampler() }, { "MitchellNetravali", new MitchellNetravaliResampler() }, + { "NearestNeighbor", new NearestNeighborResampler() }, { "Hermite", new HermiteResampler() }, { "Spline", new SplineResampler() }, { "Robidoux", new RobidouxResampler() }, @@ -33,9 +34,9 @@ namespace ImageProcessor.Tests [MemberData("Samplers")] public void ImageShouldResize(string name, IResampler sampler) { - if (!Directory.Exists("TestOutput/Resized")) + if (!Directory.Exists("TestOutput/Resize")) { - Directory.CreateDirectory("TestOutput/Resized"); + Directory.CreateDirectory("TestOutput/Resize"); } foreach (string file in Files) @@ -45,7 +46,7 @@ namespace ImageProcessor.Tests Stopwatch watch = Stopwatch.StartNew(); Image image = new Image(stream); string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); - using (FileStream output = File.OpenWrite($"TestOutput/Resized/{filename}")) + using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}")) { image.Resize(image.Width / 2, image.Height / 2, sampler) .Save(output); @@ -56,6 +57,33 @@ namespace ImageProcessor.Tests } } + [Theory] + [MemberData("Samplers")] + public void ImageShouldRotate(string name, IResampler sampler) + { + if (!Directory.Exists("TestOutput/Rotate")) + { + Directory.CreateDirectory("TestOutput/Rotate"); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + Stopwatch watch = Stopwatch.StartNew(); + Image image = new Image(stream); + string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); + using (FileStream output = File.OpenWrite($"TestOutput/Rotate/{filename}")) + { + image.Rotate(45, sampler) + .Save(output); + } + + Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms"); + } + } + } + [Fact] public void ImageShouldEntropyCrop() { @@ -81,9 +109,9 @@ namespace ImageProcessor.Tests [Fact] public void ImageShouldCrop() { - if (!Directory.Exists("TestOutput/Cropped")) + if (!Directory.Exists("TestOutput/Crop")) { - Directory.CreateDirectory("TestOutput/Cropped"); + Directory.CreateDirectory("TestOutput/Crop"); } foreach (string file in Files) @@ -91,8 +119,8 @@ namespace ImageProcessor.Tests using (FileStream stream = File.OpenRead(file)) { Image image = new Image(stream); - string filename = Path.GetFileNameWithoutExtension(file) + "-Cropped" + Path.GetExtension(file); - using (FileStream output = File.OpenWrite($"TestOutput/Cropped/{filename}")) + string filename = Path.GetFileNameWithoutExtension(file) + "-Crop" + Path.GetExtension(file); + using (FileStream output = File.OpenWrite($"TestOutput/Crop/{filename}")) { image.Crop(image.Width / 2, image.Height / 2).Save(output); }