Browse Source

Resizer is now a 2-Pass 1D process.

Former-commit-id: f7dddf44418638397c4d3639a08fc1058d720470
Former-commit-id: 5a08b1e1bdedc910a00f66917bf1ac3eadaa7a31
Former-commit-id: f3be022b70d7e2edf4c1741566008abe7b034a1e
af/merge-core
James Jackson-South 10 years ago
parent
commit
ecf90be3d5
  1. 9
      src/ImageProcessor/Samplers/ImageSampleExtensions.cs
  2. 355
      src/ImageProcessor/Samplers/Resampler.cs
  3. 2
      src/ImageProcessor/Samplers/Resamplers/BoxResampler.cs
  4. 4
      src/ImageProcessor/Samplers/Resamplers/RobidouxResampler.cs
  5. 4
      src/ImageProcessor/Samplers/Resamplers/RobidouxSharpResampler.cs
  6. 158
      src/ImageProcessor/Samplers/Resize.cs
  7. 159
      src/ImageProcessor/Samplers/Rotate.cs

9
src/ImageProcessor/Samplers/ImageSampleExtensions.cs

@ -69,7 +69,7 @@ namespace ImageProcessor.Samplers
/// <remarks>Passing zero for one of height or width will automatically preserve the aspect ratio of the original image</remarks>
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());
}
/// <summary>
@ -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));
}
/// <summary>
@ -121,7 +122,7 @@ namespace ImageProcessor.Samplers
/// <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 BicubicResampler()) { Angle = degrees });
return Rotate(source, degrees, new BicubicResampler());
}
/// <summary>
@ -133,7 +134,7 @@ namespace ImageProcessor.Samplers
/// <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 });
return source.Process(source.Width, source.Height, source.Bounds, source.Bounds, new Rotate(sampler) { Angle = degrees });
}
/// <summary>

355
src/ImageProcessor/Samplers/Resampler.cs

@ -12,30 +12,15 @@ namespace ImageProcessor.Samplers
/// <summary>
/// Provides methods that allow the resampling of images using various algorithms.
/// </summary>
public class Resampler : ParallelImageProcessor
public abstract class Resampler : ParallelImageProcessor
{
/// <summary>
/// The angle of rotation.
/// </summary>
private float angle;
/// <summary>
/// The horizontal weights.
/// </summary>
private Weights[] horizontalWeights;
/// <summary>
/// The vertical weights.
/// </summary>
private Weights[] verticalWeights;
/// <summary>
/// Initializes a new instance of the <see cref="Resampler"/> class.
/// </summary>
/// <param name="sampler">
/// The sampler to perform the resize operation.
/// </param>
public Resampler(IResampler sampler)
protected Resampler(IResampler sampler)
{
Guard.NotNull(sampler, nameof(sampler));
@ -48,272 +33,14 @@ namespace ImageProcessor.Samplers
public IResampler Sampler { get; }
/// <summary>
/// Gets or sets the angle of rotation.
/// </summary>
public float Angle
{
get
{
return this.angle;
}
set
{
if (value > 360)
{
value -= 360;
}
if (value < 0)
{
value += 360;
}
this.angle = value;
}
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
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);
}
}
/// <summary>
/// Resamples the specified <see cref="ImageBase"/> at the specified location
/// and with the specified size.
/// Gets or sets the horizontal weights.
/// </summary>
/// <param name="target">Target image to apply the process to.</param>
/// <param name="source">The source image. Cannot be null.</param>
/// <param name="targetRectangle">
/// The <see cref="Rectangle"/> structure that specifies the location and size of the drawn image.
/// The image is scaled to fit the rectangle.
/// </param>
/// <param name="startY">The index of the row within the source image to start processing.</param>
/// <param name="endY">The index of the row within the source image to end processing.</param>
/// <remarks>
/// The method keeps the source image unchanged and returns the
/// the result of image process as new image.
/// </remarks>
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; }
/// <summary>
/// Resamples and rotates the specified <see cref="ImageBase"/> at the specified location
/// and with the specified size.
/// Gets or sets the vertical weights.
/// </summary>
/// <param name="target">Target image to apply the process to.</param>
/// <param name="source">The source image. Cannot be null.</param>
/// <param name="targetRectangle">
/// The <see cref="Rectangle"/> structure that specifies the location and size of the drawn image.
/// The image is scaled to fit the rectangle.
/// </param>
/// <param name="sourceRectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to draw.
/// </param>
/// <param name="startY">The index of the row within the source image to start processing.</param>
/// <param name="endY">The index of the row within the source image to end processing.</param>
/// <remarks>
/// The method keeps the source image unchanged and returns the
/// the result of image process as new image.
/// </remarks>
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; }
/// <summary>
/// Computes the weights to apply at each pixel when resizing.
@ -323,7 +50,7 @@ namespace ImageProcessor.Samplers
/// <returns>
/// The <see cref="T:Weights[]"/>.
/// </returns>
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<Weight> builder = new List<Weight>();
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<Weight> builder = new List<Weight>();
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; }
}
}
}
}

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

@ -12,7 +12,7 @@ namespace ImageProcessor.Samplers
public class BoxResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 0.5f;
public float Radius => 0.5F;
/// <inheritdoc/>
public float GetValue(float x)

4
src/ImageProcessor/Samplers/Resamplers/RobidouxResampler.cs

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

4
src/ImageProcessor/Samplers/Resamplers/RobidouxSharpResampler.cs

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

158
src/ImageProcessor/Samplers/Resize.cs

@ -0,0 +1,158 @@
// <copyright file="Resize.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessor.Samplers
{
using System.Threading.Tasks;
/// <summary>
/// Provides methods that allow the resizing of images using various algorithms.
/// </summary>
public class Resize : Resampler
{
/// <summary>
/// The image used for storing the first pass pixels.
/// </summary>
private Image firstPass;
/// <summary>
/// Initializes a new instance of the <see cref="Resize"/> class.
/// </summary>
/// <param name="sampler">
/// The sampler to perform the resize operation.
/// </param>
public Resize(IResampler sampler)
: base(sampler)
{
}
/// <inheritdoc/>
public override int Parallelism { get; set; } = 1;
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
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;
}
}
});
}
/// <inheritdoc/>
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);
}
}
}
}

159
src/ImageProcessor/Samplers/Rotate.cs

@ -0,0 +1,159 @@
// <copyright file="Rotate.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessor.Samplers
{
using System.Threading.Tasks;
/// <summary>
/// Provides methods that allow the rotating of images using various algorithms.
/// </summary>
public class Rotate : Resampler
{
/// <summary>
/// The angle of rotation.
/// </summary>
private float angle;
/// <summary>
/// Initializes a new instance of the <see cref="Rotate"/> class.
/// </summary>
/// <param name="sampler">
/// The sampler to perform the resize operation.
/// </param>
public Rotate(IResampler sampler)
: base(sampler)
{
}
/// <summary>
/// Gets or sets the angle of rotation.
/// </summary>
public float Angle
{
get
{
return this.angle;
}
set
{
if (value > 360)
{
value -= 360;
}
if (value < 0)
{
value += 360;
}
this.angle = value;
}
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
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;
}
}
});
}
}
}
Loading…
Cancel
Save