Browse Source

Add nearest neighbour resampler #257

Former-commit-id: 8e6ac4bf4ee07615c7bb707aedc12afccd1647af
Former-commit-id: 40db8d0882d4974b675fd0cd7531eabb7c701210
Former-commit-id: 99fdd9fb8851e2e23510f3682f671acf06620a31
af/merge-core
James Jackson-South 10 years ago
parent
commit
bab6350aaa
  1. 14
      src/ImageProcessor/Samplers/ImageSampleExtensions.cs
  2. 84
      src/ImageProcessor/Samplers/Resampler.cs
  3. 1
      src/ImageProcessor/Samplers/Resamplers/BicubicResampler.cs
  4. 3
      src/ImageProcessor/Samplers/Resamplers/BoxResampler.cs
  5. 23
      src/ImageProcessor/Samplers/Resamplers/NearestNeighborResampler.cs
  6. 2
      src/ImageProcessor/Samplers/Resamplers/TriangleResampler.cs
  7. 42
      tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs

14
src/ImageProcessor/Samplers/ImageSampleExtensions.cs

@ -105,11 +105,23 @@ namespace ImageProcessor.Samplers
/// 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 porform the rotation.</param>
/// <param name="degrees">The angle in degrees to perform the rotation.</param>
/// <returns>The <see cref="Image"/></returns>
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 });
}
/// <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>
/// <returns>The <see cref="Image"/></returns>
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 });
}
}
}

84
src/ImageProcessor/Samplers/Resampler.cs

@ -77,8 +77,11 @@ namespace ImageProcessor.Samplers
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
@ -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;
}
}
}

1
src/ImageProcessor/Samplers/Resamplers/BicubicResampler.cs

@ -8,6 +8,7 @@ namespace ImageProcessor.Samplers
/// <summary>
/// The function implements the bicubic kernel algorithm W(x) as described on
/// <see href="https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm">Wikipedia</see>
/// A commonly used algorithm within imageprocessing that preserves sharpness better than triangle interpolation.
/// </summary>
public class BicubicResampler : IResampler
{

3
src/ImageProcessor/Samplers/Resamplers/BoxResampler.cs

@ -6,7 +6,8 @@
namespace ImageProcessor.Samplers
{
/// <summary>
/// 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.
/// </summary>
public class BoxResampler : IResampler
{

23
src/ImageProcessor/Samplers/Resamplers/NearestNeighborResampler.cs

@ -0,0 +1,23 @@
// <copyright file="NearestNeighborResampler.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessor.Samplers
{
/// <summary>
/// The function implements the nearest neighbour algorithm. This uses an unscaled filter
/// which will select the closest pixel to the new pixels position.
/// </summary>
public class NearestNeighborResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 1;
/// <inheritdoc/>
public float GetValue(float x)
{
return x;
}
}
}

2
src/ImageProcessor/Samplers/Resamplers/TriangleResampler.cs

@ -7,6 +7,8 @@ namespace ImageProcessor.Samplers
{
/// <summary>
/// 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.
/// </summary>
public class TriangleResampler : IResampler
{

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

Loading…
Cancel
Save