Browse Source

Split out resizers to improve performance.

Former-commit-id: 1871483f9d145b59268bc94f9acac00db152fe05
Former-commit-id: f09e24bcae7cc31550084e20e843f53f77c2045a
Former-commit-id: ac0732d1617f74808c6680ad4e52d7f9ba916b49
af/merge-core
James Jackson-South 10 years ago
parent
commit
98ad93304d
  1. 177
      src/ImageProcessorCore/Samplers/Processors/CompandingResizeProcessor.cs
  2. 209
      src/ImageProcessorCore/Samplers/Processors/ResamplingWeightedProcessor.cs
  3. 210
      src/ImageProcessorCore/Samplers/Processors/ResizeProcessor.cs
  4. 12
      src/ImageProcessorCore/Samplers/Resize.cs
  5. 8
      tests/ImageProcessorCore.Benchmarks/Samplers/Resize.cs
  6. 2
      tests/ImageProcessorCore.Tests/Processors/Samplers/ResizeTests.cs

177
src/ImageProcessorCore/Samplers/Processors/CompandingResizeProcessor.cs

@ -0,0 +1,177 @@
// <copyright file="CompandingResizeProcessor.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Processors
{
using System.Numerics;
using System.Threading.Tasks;
/// <summary>
/// Provides methods that allow the resizing of images using various algorithms.
/// This version will expand and compress the image to and from a linear color space during processing.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
public class CompandingResizeProcessor<T, TP> : ResamplingWeightedProcessor<T, TP>
where T : IPackedVector<TP>
where TP : struct
{
/// <summary>
/// Initializes a new instance of the <see cref="CompandingResizeProcessor{T,TP}"/> class.
/// </summary>
/// <param name="sampler">
/// The sampler to perform the resize operation.
/// </param>
public CompandingResizeProcessor(IResampler sampler)
: base(sampler)
{
}
/// <inheritdoc/>
public override bool Compand { get; set; } = true;
/// <inheritdoc/>
protected override void Apply(ImageBase<T, TP> target, ImageBase<T, TP> source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY)
{
// Jump out, we'll deal with that later.
if (source.Bounds == target.Bounds && sourceRectangle == targetRectangle)
{
return;
}
int width = target.Width;
int height = target.Height;
int sourceHeight = sourceRectangle.Height;
int targetX = target.Bounds.X;
int targetY = target.Bounds.Y;
int targetRight = target.Bounds.Right;
int targetBottom = target.Bounds.Bottom;
int startX = targetRectangle.X;
int endX = targetRectangle.Right;
bool compand = this.Compand;
if (this.Sampler is NearestNeighborResampler)
{
// Scaling factors
float widthFactor = sourceRectangle.Width / (float)targetRectangle.Width;
float heightFactor = sourceRectangle.Height / (float)targetRectangle.Height;
using (IPixelAccessor<T, TP> sourcePixels = source.Lock())
using (IPixelAccessor<T, TP> targetPixels = target.Lock())
{
Parallel.For(
startY,
endY,
this.ParallelOptions,
y =>
{
if (targetY <= y && y < targetBottom)
{
// Y coordinates of source points
int originY = (int)((y - startY) * heightFactor);
for (int x = startX; x < endX; x++)
{
if (targetX <= x && x < targetRight)
{
// X coordinates of source points
int originX = (int)((x - startX) * widthFactor);
targetPixels[x, y] = sourcePixels[originX, originY];
}
}
this.OnRowProcessed();
}
});
}
// 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. Since we are not using multiple threads startY and endY
// are the upper and lower bounds of the source rectangle.
Image<T, TP> firstPass = new Image<T, TP>(target.Width, source.Height);
using (IPixelAccessor<T, TP> sourcePixels = source.Lock())
using (IPixelAccessor<T, TP> firstPassPixels = firstPass.Lock())
using (IPixelAccessor<T, TP> targetPixels = target.Lock())
{
Parallel.For(
0,
sourceHeight,
this.ParallelOptions,
y =>
{
for (int x = startX; x < endX; x++)
{
if (x >= 0 && x < width)
{
// Ensure offsets are normalised for cropping and padding.
int offsetX = x - startX;
double sum = this.HorizontalWeights[offsetX].Sum;
Weight[] horizontalValues = this.HorizontalWeights[offsetX].Values;
// Destination color components
Vector4 destination = Vector4.Zero;
for (int i = 0; i < sum; i++)
{
Weight xw = horizontalValues[i];
int originX = xw.Index;
Vector4 sourceColor = sourcePixels[originX, y].ToVector4().Expand();
destination += sourceColor * xw.Value;
}
T d = default(T);
d.PackVector(destination.Compress());
firstPassPixels[x, y] = d;
}
}
});
// Now process the rows.
Parallel.For(
startY,
endY,
this.ParallelOptions,
y =>
{
if (y >= 0 && y < height)
{
// Ensure offsets are normalised for cropping and padding.
int offsetY = y - startY;
double sum = this.VerticalWeights[offsetY].Sum;
Weight[] verticalValues = this.VerticalWeights[offsetY].Values;
for (int x = 0; x < width; x++)
{
// Destination color components
Vector4 destination = Vector4.Zero;
for (int i = 0; i < sum; i++)
{
Weight yw = verticalValues[i];
int originY = yw.Index;
Vector4 sourceColor = firstPassPixels[x, originY].ToVector4().Expand();
destination += sourceColor * yw.Value;
}
T d = default(T);
d.PackVector(destination.Compress());
targetPixels[x, y] = d;
}
}
this.OnRowProcessed();
});
}
}
}
}

209
src/ImageProcessorCore/Samplers/Processors/ResamplingWeightedProcessor.cs

@ -0,0 +1,209 @@
// <copyright file="ResamplingWeightedProcessor.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Processors
{
using System;
/// <summary>
/// Provides methods that allow the resizing of images using various algorithms.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
public abstract class ResamplingWeightedProcessor<T, TP> : ImageSampler<T, TP>
where T : IPackedVector<TP>
where TP : struct
{
/// <summary>
/// Initializes a new instance of the <see cref="ResamplingWeightedProcessor{T,TP}"/> class.
/// </summary>
/// <param name="sampler">
/// The sampler to perform the resize operation.
/// </param>
protected ResamplingWeightedProcessor(IResampler sampler)
{
Guard.NotNull(sampler, nameof(sampler));
this.Sampler = sampler;
}
/// <summary>
/// Gets the sampler to perform the resize operation.
/// </summary>
public IResampler Sampler { get; }
/// <summary>
/// Gets or sets the horizontal weights.
/// </summary>
protected Weights[] HorizontalWeights { get; set; }
/// <summary>
/// Gets or sets the vertical weights.
/// </summary>
protected Weights[] VerticalWeights { get; set; }
/// <inheritdoc/>
protected override void OnApply(ImageBase<T, TP> target, ImageBase<T, TP> source, 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 AfterApply(ImageBase<T, TP> target, ImageBase<T, TP> source, Rectangle targetRectangle, Rectangle sourceRectangle)
{
// Copy the pixels over.
if (source.Bounds == target.Bounds && sourceRectangle == targetRectangle)
{
target.ClonePixels(target.Width, target.Height, source.Pixels);
}
}
/// <summary>
/// Computes the weights to apply at each pixel when resizing.
/// </summary>
/// <param name="destinationSize">The destination section size.</param>
/// <param name="sourceSize">The source section size.</param>
/// <returns>
/// The <see cref="T:Weights[]"/>.
/// </returns>
protected Weights[] PrecomputeWeights(int destinationSize, int sourceSize)
{
float scale = (float)destinationSize / sourceSize;
IResampler sampler = this.Sampler;
float radius = sampler.Radius;
double left;
double right;
float weight;
int index;
int sum;
Weights[] result = new Weights[destinationSize];
// When shrinking, broaden the effective kernel support so that we still
// visit every source pixel.
if (scale < 1)
{
float width = radius / scale;
float filterScale = 1 / scale;
// Make the weights slices, one source for each column or row.
for (int i = 0; i < destinationSize; i++)
{
float centre = i / scale;
left = Math.Ceiling(centre - width);
right = Math.Floor(centre + width);
result[i] = new Weights
{
Values = new Weight[(int)(right - left + 1)]
};
for (double j = left; j <= right; j++)
{
weight = sampler.GetValue((float)((centre - j) / filterScale)) / filterScale;
if (j < 0)
{
index = (int)-j;
}
else if (j >= sourceSize)
{
index = (int)((sourceSize - j) + sourceSize - 1);
}
else
{
index = (int)j;
}
sum = (int)result[i].Sum++;
result[i].Values[sum] = new Weight(index, weight);
}
}
}
else
{
// Make the weights slices, one source for each column or row.
for (int i = 0; i < destinationSize; i++)
{
float centre = i / scale;
left = Math.Ceiling(centre - radius);
right = Math.Floor(centre + radius);
result[i] = new Weights
{
Values = new Weight[(int)(right - left + 1)]
};
for (double j = left; j <= right; j++)
{
weight = sampler.GetValue((float)(centre - j));
if (j < 0)
{
index = (int)-j;
}
else if (j >= sourceSize)
{
index = (int)((sourceSize - j) + sourceSize - 1);
}
else
{
index = (int)j;
}
sum = (int)result[i].Sum++;
result[i].Values[sum] = new Weight(index, weight);
}
}
}
return result;
}
/// <summary>
/// Represents the weight to be added to a scaled pixel.
/// </summary>
protected struct Weight
{
/// <summary>
/// Initializes a new instance of the <see cref="Weight"/> struct.
/// </summary>
/// <param name="index">The index.</param>
/// <param name="value">The value.</param>
public Weight(int index, float value)
{
this.Index = index;
this.Value = value;
}
/// <summary>
/// Gets the pixel index.
/// </summary>
public int Index { get; }
/// <summary>
/// Gets the result of the interpolation algorithm.
/// </summary>
public float Value { get; }
}
/// <summary>
/// Represents a collection of weights and their sum.
/// </summary>
protected class Weights
{
/// <summary>
/// Gets or sets the values.
/// </summary>
public Weight[] Values { get; set; }
/// <summary>
/// Gets or sets the sum.
/// </summary>
public float Sum { get; set; }
}
}
}

210
src/ImageProcessorCore/Samplers/Processors/ResizeProcessor.cs

@ -5,55 +5,30 @@
namespace ImageProcessorCore.Processors
{
using System;
using System.Numerics;
using System.Threading.Tasks;
/// <summary>
/// Provides methods that allow the resizing of images using various algorithms.
/// </summary>
/// <remarks>
/// This version and the <see cref="CompandingResizeProcessor{T,TP}"/> have been separated out to improve performance.
/// </remarks>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
public class ResizeProcessor<T, TP> : ImageSampler<T, TP>
public class ResizeProcessor<T, TP> : ResamplingWeightedProcessor<T, TP>
where T : IPackedVector<TP>
where TP : struct
{
/// <summary>
/// Initializes a new instance of the <see cref="ResizeProcessor"/> class.
/// Initializes a new instance of the <see cref="ResizeProcessor{T,TP}"/> class.
/// </summary>
/// <param name="sampler">
/// The sampler to perform the resize operation.
/// </param>
public ResizeProcessor(IResampler sampler)
: base(sampler)
{
Guard.NotNull(sampler, nameof(sampler));
this.Sampler = sampler;
}
/// <summary>
/// Gets the sampler to perform the resize operation.
/// </summary>
public IResampler Sampler { get; }
/// <summary>
/// Gets or sets the horizontal weights.
/// </summary>
protected Weights[] HorizontalWeights { get; set; }
/// <summary>
/// Gets or sets the vertical weights.
/// </summary>
protected Weights[] VerticalWeights { get; set; }
/// <inheritdoc/>
protected override void OnApply(ImageBase<T, TP> target, ImageBase<T, TP> source, 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/>
@ -74,7 +49,6 @@ namespace ImageProcessorCore.Processors
int targetBottom = target.Bounds.Bottom;
int startX = targetRectangle.X;
int endX = targetRectangle.Right;
bool compand = this.Compand;
if (this.Sampler is NearestNeighborResampler)
{
@ -148,19 +122,9 @@ namespace ImageProcessorCore.Processors
int originX = xw.Index;
Vector4 sourceColor = sourcePixels[originX, y].ToVector4();
if (compand)
{
sourceColor = sourceColor.Expand();
}
destination += sourceColor * xw.Value;
}
if (compand)
{
destination = destination.Compress();
}
T d = default(T);
d.PackVector(destination);
firstPassPixels[x, y] = d;
@ -193,19 +157,9 @@ namespace ImageProcessorCore.Processors
int originY = yw.Index;
Vector4 sourceColor = firstPassPixels[x, originY].ToVector4();
if (compand)
{
sourceColor = sourceColor.Expand();
}
destination += sourceColor * yw.Value;
}
if (compand)
{
destination = destination.Compress();
}
T d = default(T);
d.PackVector(destination);
targetPixels[x, y] = d;
@ -217,157 +171,5 @@ namespace ImageProcessorCore.Processors
}
}
/// <inheritdoc/>
protected override void AfterApply(ImageBase<T, TP> target, ImageBase<T, TP> source, Rectangle targetRectangle, Rectangle sourceRectangle)
{
// Copy the pixels over.
if (source.Bounds == target.Bounds && sourceRectangle == targetRectangle)
{
target.ClonePixels(target.Width, target.Height, source.Pixels);
}
}
/// <summary>
/// Computes the weights to apply at each pixel when resizing.
/// </summary>
/// <param name="destinationSize">The destination section size.</param>
/// <param name="sourceSize">The source section size.</param>
/// <returns>
/// The <see cref="T:Weights[]"/>.
/// </returns>
protected Weights[] PrecomputeWeights(int destinationSize, int sourceSize)
{
float scale = (float)destinationSize / sourceSize;
IResampler sampler = this.Sampler;
float radius = sampler.Radius;
double left;
double right;
float weight;
int index;
int sum;
Weights[] result = new Weights[destinationSize];
// When shrinking, broaden the effective kernel support so that we still
// visit every source pixel.
if (scale < 1)
{
float width = radius / scale;
float filterScale = 1 / scale;
// Make the weights slices, one source for each column or row.
for (int i = 0; i < destinationSize; i++)
{
float centre = i / scale;
left = Math.Ceiling(centre - width);
right = Math.Floor(centre + width);
result[i] = new Weights
{
Values = new Weight[(int)(right - left + 1)]
};
for (double j = left; j <= right; j++)
{
weight = sampler.GetValue((float)((centre - j) / filterScale)) / filterScale;
if (j < 0)
{
index = (int)-j;
}
else if (j >= sourceSize)
{
index = (int)((sourceSize - j) + sourceSize - 1);
}
else
{
index = (int)j;
}
sum = (int)result[i].Sum++;
result[i].Values[sum] = new Weight(index, weight);
}
}
}
else
{
// Make the weights slices, one source for each column or row.
for (int i = 0; i < destinationSize; i++)
{
float centre = i / scale;
left = Math.Ceiling(centre - radius);
right = Math.Floor(centre + radius);
result[i] = new Weights
{
Values = new Weight[(int)(right - left + 1)]
};
for (double j = left; j <= right; j++)
{
weight = sampler.GetValue((float)(centre - j));
if (j < 0)
{
index = (int)-j;
}
else if (j >= sourceSize)
{
index = (int)((sourceSize - j) + sourceSize - 1);
}
else
{
index = (int)j;
}
sum = (int)result[i].Sum++;
result[i].Values[sum] = new Weight(index, weight);
}
}
}
return result;
}
/// <summary>
/// Represents the weight to be added to a scaled pixel.
/// </summary>
protected struct Weight
{
/// <summary>
/// Initializes a new instance of the <see cref="Weight"/> struct.
/// </summary>
/// <param name="index">The index.</param>
/// <param name="value">The value.</param>
public Weight(int index, float value)
{
this.Index = index;
this.Value = value;
}
/// <summary>
/// Gets the pixel index.
/// </summary>
public int Index { get; }
/// <summary>
/// Gets the result of the interpolation algorithm.
/// </summary>
public float Value { get; }
}
/// <summary>
/// Represents a collection of weights and their sum.
/// </summary>
protected class Weights
{
/// <summary>
/// Gets or sets the values.
/// </summary>
public Weight[] Values { get; set; }
/// <summary>
/// Gets or sets the sum.
/// </summary>
public float Sum { get; set; }
}
}
}

12
src/ImageProcessorCore/Samplers/Resize.cs

@ -138,7 +138,17 @@ namespace ImageProcessorCore
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
ResizeProcessor<T, TP> processor = new ResizeProcessor<T, TP>(sampler) { Compand = compand };
ResamplingWeightedProcessor<T, TP> processor;
if (compand)
{
processor = new CompandingResizeProcessor<T, TP>(sampler);
}
else
{
processor = new ResizeProcessor<T, TP>(sampler);
}
processor.OnProgress += progressHandler;
try

8
tests/ImageProcessorCore.Benchmarks/Samplers/Resize.cs

@ -36,5 +36,13 @@
image.Resize(400, 400);
return new CoreSize(image.Width, image.Height);
}
[Benchmark(Description = "ImageProcessorCore Compand Resize")]
public CoreSize ResizeCore()
{
CoreImage image = new CoreImage(2000, 2000);
image.Resize(400, 400, true);
return new CoreSize(image.Width, image.Height);
}
}
}

2
tests/ImageProcessorCore.Tests/Processors/Samplers/ResizeTests.cs

@ -51,7 +51,7 @@ namespace ImageProcessorCore.Tests
Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Resize(image.Width / 2, image.Height / 2, sampler, false, this.ProgressUpdate)
image.Resize(image.Width / 2, image.Height / 2, sampler, true, this.ProgressUpdate)
.Save(output);
}
}

Loading…
Cancel
Save