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);
}