From 98ad93304d61a41e01663d52b28461248c541b6a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 5 Aug 2016 17:00:54 +1000 Subject: [PATCH] Split out resizers to improve performance. Former-commit-id: 1871483f9d145b59268bc94f9acac00db152fe05 Former-commit-id: f09e24bcae7cc31550084e20e843f53f77c2045a Former-commit-id: ac0732d1617f74808c6680ad4e52d7f9ba916b49 --- .../Processors/CompandingResizeProcessor.cs | 177 +++++++++++++++ .../Processors/ResamplingWeightedProcessor.cs | 209 +++++++++++++++++ .../Samplers/Processors/ResizeProcessor.cs | 210 +----------------- src/ImageProcessorCore/Samplers/Resize.cs | 12 +- .../Samplers/Resize.cs | 8 + .../Processors/Samplers/ResizeTests.cs | 2 +- 6 files changed, 412 insertions(+), 206 deletions(-) create mode 100644 src/ImageProcessorCore/Samplers/Processors/CompandingResizeProcessor.cs create mode 100644 src/ImageProcessorCore/Samplers/Processors/ResamplingWeightedProcessor.cs diff --git a/src/ImageProcessorCore/Samplers/Processors/CompandingResizeProcessor.cs b/src/ImageProcessorCore/Samplers/Processors/CompandingResizeProcessor.cs new file mode 100644 index 0000000000..23887af23b --- /dev/null +++ b/src/ImageProcessorCore/Samplers/Processors/CompandingResizeProcessor.cs @@ -0,0 +1,177 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Processors +{ + using System.Numerics; + using System.Threading.Tasks; + + /// + /// 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. + /// + /// The pixel format. + /// The packed format. long, float. + public class CompandingResizeProcessor : ResamplingWeightedProcessor + where T : IPackedVector + where TP : struct + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The sampler to perform the resize operation. + /// + public CompandingResizeProcessor(IResampler sampler) + : base(sampler) + { + } + + /// + public override bool Compand { get; set; } = true; + + /// + 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 && 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 sourcePixels = source.Lock()) + using (IPixelAccessor 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 firstPass = new Image(target.Width, source.Height); + using (IPixelAccessor sourcePixels = source.Lock()) + using (IPixelAccessor firstPassPixels = firstPass.Lock()) + using (IPixelAccessor 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(); + }); + + } + } + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Samplers/Processors/ResamplingWeightedProcessor.cs b/src/ImageProcessorCore/Samplers/Processors/ResamplingWeightedProcessor.cs new file mode 100644 index 0000000000..5630ec431f --- /dev/null +++ b/src/ImageProcessorCore/Samplers/Processors/ResamplingWeightedProcessor.cs @@ -0,0 +1,209 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Processors +{ + using System; + + /// + /// Provides methods that allow the resizing of images using various algorithms. + /// + /// The pixel format. + /// The packed format. long, float. + public abstract class ResamplingWeightedProcessor : ImageSampler + where T : IPackedVector + where TP : struct + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The sampler to perform the resize operation. + /// + protected ResamplingWeightedProcessor(IResampler sampler) + { + Guard.NotNull(sampler, nameof(sampler)); + + this.Sampler = sampler; + } + + /// + /// Gets the sampler to perform the resize operation. + /// + public IResampler Sampler { get; } + + /// + /// Gets or sets the horizontal weights. + /// + protected Weights[] HorizontalWeights { get; set; } + + /// + /// Gets or sets the vertical weights. + /// + protected Weights[] VerticalWeights { get; set; } + + /// + protected override void OnApply(ImageBase target, ImageBase 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); + } + } + + /// + protected override void AfterApply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle) + { + // Copy the pixels over. + if (source.Bounds == target.Bounds && sourceRectangle == targetRectangle) + { + target.ClonePixels(target.Width, target.Height, source.Pixels); + } + } + + /// + /// Computes the weights to apply at each pixel when resizing. + /// + /// The destination section size. + /// The source section size. + /// + /// The . + /// + 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; + } + + /// + /// Represents the weight to be added to a scaled pixel. + /// + protected struct Weight + { + /// + /// Initializes a new instance of the struct. + /// + /// The index. + /// The value. + public Weight(int index, float value) + { + this.Index = index; + this.Value = value; + } + + /// + /// Gets the pixel index. + /// + public int Index { get; } + + /// + /// Gets the result of the interpolation algorithm. + /// + public float Value { get; } + } + + /// + /// Represents a collection of weights and their sum. + /// + protected class Weights + { + /// + /// Gets or sets the values. + /// + public Weight[] Values { get; set; } + + /// + /// Gets or sets the sum. + /// + public float Sum { get; set; } + } + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Samplers/Processors/ResizeProcessor.cs b/src/ImageProcessorCore/Samplers/Processors/ResizeProcessor.cs index b912a53eee..da588c9978 100644 --- a/src/ImageProcessorCore/Samplers/Processors/ResizeProcessor.cs +++ b/src/ImageProcessorCore/Samplers/Processors/ResizeProcessor.cs @@ -5,55 +5,30 @@ namespace ImageProcessorCore.Processors { - using System; using System.Numerics; using System.Threading.Tasks; /// /// Provides methods that allow the resizing of images using various algorithms. /// + /// + /// This version and the have been separated out to improve performance. + /// /// The pixel format. /// The packed format. long, float. - public class ResizeProcessor : ImageSampler + public class ResizeProcessor : ResamplingWeightedProcessor where T : IPackedVector where TP : struct { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// The sampler to perform the resize operation. /// public ResizeProcessor(IResampler sampler) + : base(sampler) { - Guard.NotNull(sampler, nameof(sampler)); - - this.Sampler = sampler; - } - - /// - /// Gets the sampler to perform the resize operation. - /// - public IResampler Sampler { get; } - - /// - /// Gets or sets the horizontal weights. - /// - protected Weights[] HorizontalWeights { get; set; } - - /// - /// Gets or sets the vertical weights. - /// - protected Weights[] VerticalWeights { get; set; } - - /// - protected override void OnApply(ImageBase target, ImageBase 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); - } } /// @@ -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 } } - - /// - protected override void AfterApply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle) - { - // Copy the pixels over. - if (source.Bounds == target.Bounds && sourceRectangle == targetRectangle) - { - target.ClonePixels(target.Width, target.Height, source.Pixels); - } - } - - /// - /// Computes the weights to apply at each pixel when resizing. - /// - /// The destination section size. - /// The source section size. - /// - /// The . - /// - 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; - } - - /// - /// Represents the weight to be added to a scaled pixel. - /// - protected struct Weight - { - /// - /// Initializes a new instance of the struct. - /// - /// The index. - /// The value. - public Weight(int index, float value) - { - this.Index = index; - this.Value = value; - } - - /// - /// Gets the pixel index. - /// - public int Index { get; } - - /// - /// Gets the result of the interpolation algorithm. - /// - public float Value { get; } - } - - /// - /// Represents a collection of weights and their sum. - /// - protected class Weights - { - /// - /// Gets or sets the values. - /// - public Weight[] Values { get; set; } - - /// - /// Gets or sets the sum. - /// - public float Sum { get; set; } - } } } \ No newline at end of file diff --git a/src/ImageProcessorCore/Samplers/Resize.cs b/src/ImageProcessorCore/Samplers/Resize.cs index 40b07469ec..5fa2eff702 100644 --- a/src/ImageProcessorCore/Samplers/Resize.cs +++ b/src/ImageProcessorCore/Samplers/Resize.cs @@ -138,7 +138,17 @@ namespace ImageProcessorCore Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); - ResizeProcessor processor = new ResizeProcessor(sampler) { Compand = compand }; + ResamplingWeightedProcessor processor; + + if (compand) + { + processor = new CompandingResizeProcessor(sampler); + } + else + { + processor = new ResizeProcessor(sampler); + } + processor.OnProgress += progressHandler; try diff --git a/tests/ImageProcessorCore.Benchmarks/Samplers/Resize.cs b/tests/ImageProcessorCore.Benchmarks/Samplers/Resize.cs index e5eb412f29..1312a91ad3 100644 --- a/tests/ImageProcessorCore.Benchmarks/Samplers/Resize.cs +++ b/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); + } } } diff --git a/tests/ImageProcessorCore.Tests/Processors/Samplers/ResizeTests.cs b/tests/ImageProcessorCore.Tests/Processors/Samplers/ResizeTests.cs index dbd36bee7a..cdf5ae8b90 100644 --- a/tests/ImageProcessorCore.Tests/Processors/Samplers/ResizeTests.cs +++ b/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); } }