From 1f32cace7e7de835e10dce4813ab88ce30aeebc0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 23 May 2016 17:59:09 +1000 Subject: [PATCH] Finish resize modes [skip ci] Former-commit-id: 7f2b4f6be701e280da5697663ecfb33daead5c8f Former-commit-id: 3a71ce1ae1387d1647a2fefc3f53122a5b717c3b Former-commit-id: 6b203558a57cd9623a67982213e004f5d068763f --- src/ImageProcessorCore/IImageBase.cs | 3 +- src/ImageProcessorCore/ImageBase.cs | 3 +- .../Samplers/ImageSamplerExtensions.cs | 2 +- src/ImageProcessorCore/Samplers/Resize.cs | 13 +- .../Samplers/ResizeHelper.cs | 174 +++++++++++++++++- .../Processors/Samplers/SamplerTests.cs | 103 ++++++++++- 6 files changed, 285 insertions(+), 13 deletions(-) diff --git a/src/ImageProcessorCore/IImageBase.cs b/src/ImageProcessorCore/IImageBase.cs index c12406d9a..0e8756f1e 100644 --- a/src/ImageProcessorCore/IImageBase.cs +++ b/src/ImageProcessorCore/IImageBase.cs @@ -6,6 +6,7 @@ namespace ImageProcessorCore { using System; + using System.Runtime.CompilerServices; /// /// Encapsulates the basic properties and methods required to manipulate images. @@ -67,7 +68,7 @@ namespace ImageProcessorCore /// than zero and smaller than the width of the pixel. /// /// The at the specified position. - Color this[int x, int y] { get; set; } + Color this[int x, int y, [CallerLineNumber] int line = 0] { get; set; } /// /// Sets the pixel array of the image to the given value. diff --git a/src/ImageProcessorCore/ImageBase.cs b/src/ImageProcessorCore/ImageBase.cs index dd6d59c59..f1bf33e5c 100644 --- a/src/ImageProcessorCore/ImageBase.cs +++ b/src/ImageProcessorCore/ImageBase.cs @@ -6,6 +6,7 @@ namespace ImageProcessorCore { using System; + using System.Runtime.CompilerServices; using System.Runtime.InteropServices; /// @@ -134,7 +135,7 @@ namespace ImageProcessorCore public int FrameDelay { get; set; } /// - public Color this[int x, int y] + public Color this[int x, int y, [CallerLineNumber] int line = 0] { get { diff --git a/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs b/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs index 3ee9f761d..fd9623fc4 100644 --- a/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs +++ b/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs @@ -74,7 +74,7 @@ namespace ImageProcessorCore.Samplers try { - return source.Process(source.Width, source.Height, source.Bounds, source.Bounds, processor); + return source.Process(source.Width, source.Height, source.Bounds, Rectangle.Empty, processor); } finally { diff --git a/src/ImageProcessorCore/Samplers/Resize.cs b/src/ImageProcessorCore/Samplers/Resize.cs index 92952ea7b..7ee290edf 100644 --- a/src/ImageProcessorCore/Samplers/Resize.cs +++ b/src/ImageProcessorCore/Samplers/Resize.cs @@ -5,7 +5,6 @@ namespace ImageProcessorCore.Samplers { - using System; using System.Threading.Tasks; /// @@ -56,6 +55,7 @@ namespace ImageProcessorCore.Samplers int width = target.Width; int height = target.Height; + int sourceHeight = sourceRectangle.Height; int targetY = targetRectangle.Y; int targetBottom = targetRectangle.Bottom; int startX = targetRectangle.X; @@ -103,9 +103,11 @@ namespace ImageProcessorCore.Samplers endY, y => { + // Ensure offsets are normalised for cropping and padding. + int offsetY = y - startY; + for (int x = startX; x < endX; x++) { - // Ensure offsets are normalised for cropping and padding. int offsetX = x - startX; float sum = this.HorizontalWeights[offsetX].Sum; @@ -118,7 +120,7 @@ namespace ImageProcessorCore.Samplers { Weight xw = horizontalValues[i]; int originX = xw.Index; - Color sourceColor = compand ? Color.Expand(source[originX, y]) : source[originX, y]; + Color sourceColor = compand ? Color.Expand(source[originX, offsetY]) : source[originX, offsetY]; destination += sourceColor * xw.Value; } @@ -127,9 +129,9 @@ namespace ImageProcessorCore.Samplers destination = Color.Compress(destination); } - if (x >= 0 && x < width) + if (x >= 0 && x < width && offsetY >= 0 && offsetY < sourceHeight) { - this.firstPass[x, y] = destination; + this.firstPass[x, offsetY] = destination; } } }); @@ -142,7 +144,6 @@ namespace ImageProcessorCore.Samplers { // Ensure offsets are normalised for cropping and padding. int offsetY = y - startY; - float sum = this.VerticalWeights[offsetY].Sum; Weight[] verticalValues = this.VerticalWeights[offsetY].Values; diff --git a/src/ImageProcessorCore/Samplers/ResizeHelper.cs b/src/ImageProcessorCore/Samplers/ResizeHelper.cs index dc72638b4..005b43c3f 100644 --- a/src/ImageProcessorCore/Samplers/ResizeHelper.cs +++ b/src/ImageProcessorCore/Samplers/ResizeHelper.cs @@ -28,8 +28,13 @@ namespace ImageProcessorCore.Samplers { case ResizeMode.Pad: return CalculatePadRectangle(source, options); + case ResizeMode.BoxPad: + return CalculateBoxPadRectangle(source, options); + case ResizeMode.Max: + return CalculateMaxRectangle(source, options); + case ResizeMode.Min: + return CalculateMinRectangle(source, options); - // TODO: Additional modes // Default case ResizeMode.Crop default: return CalculateCropRectangle(source, options); @@ -231,5 +236,172 @@ namespace ImageProcessorCore.Samplers return new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight); } + + /// + /// Calculates the target rectangle for box pad mode. + /// + /// The source image. + /// The resize options. + /// + /// The . + /// + private static Rectangle CalculateBoxPadRectangle(ImageBase source, ResizeOptions options) + { + int width = options.Size.Width; + int height = options.Size.Height; + + if (width <= 0 || height <= 0) + { + return new Rectangle(0, 0, source.Width, source.Height); + } + + int sourceWidth = source.Width; + int sourceHeight = source.Height; + + // Fractional variants for preserving aspect ratio. + double percentHeight = Math.Abs(height / (double)sourceHeight); + double percentWidth = Math.Abs(width / (double)sourceWidth); + + int boxPadHeight = height > 0 ? height : Convert.ToInt32(sourceHeight * percentWidth); + int boxPadWidth = width > 0 ? width : Convert.ToInt32(sourceWidth * percentHeight); + + // Only calculate if upscaling. + if (sourceWidth < boxPadWidth && sourceHeight < boxPadHeight) + { + int destinationX; + int destinationY; + int destinationWidth = sourceWidth; + int destinationHeight = sourceHeight; + width = boxPadWidth; + height = boxPadHeight; + + switch (options.Position) + { + case AnchorPosition.Left: + destinationY = (height - sourceHeight) / 2; + destinationX = 0; + break; + case AnchorPosition.Right: + destinationY = (height - sourceHeight) / 2; + destinationX = width - sourceWidth; + break; + case AnchorPosition.TopRight: + destinationY = 0; + destinationX = width - sourceWidth; + break; + case AnchorPosition.Top: + destinationY = 0; + destinationX = (width - sourceWidth) / 2; + break; + case AnchorPosition.TopLeft: + destinationY = 0; + destinationX = 0; + break; + case AnchorPosition.BottomRight: + destinationY = height - sourceHeight; + destinationX = width - sourceWidth; + break; + case AnchorPosition.Bottom: + destinationY = height - sourceHeight; + destinationX = (width - sourceWidth) / 2; + break; + case AnchorPosition.BottomLeft: + destinationY = height - sourceHeight; + destinationX = 0; + break; + default: + destinationY = (height - sourceHeight) / 2; + destinationX = (width - sourceWidth) / 2; + break; + } + + return new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight); + } + + // Switch to pad mode to downscale and calculate from there. + return CalculatePadRectangle(source, options); + } + + /// + /// Calculates the target rectangle for max mode. + /// + /// The source image. + /// The resize options. + /// + /// The . + /// + private static Rectangle CalculateMaxRectangle(ImageBase source, ResizeOptions options) + { + int width = options.Size.Width; + int height = options.Size.Height; + int destinationWidth = width; + int destinationHeight = height; + + // Fractional variants for preserving aspect ratio. + double percentHeight = Math.Abs(height / (double)source.Height); + double percentWidth = Math.Abs(width / (double)source.Width); + + // Integers must be cast to doubles to get needed precision + double ratio = (double)options.Size.Height / options.Size.Width; + double sourceRatio = (double)source.Height / source.Width; + + if (sourceRatio < ratio) + { + destinationHeight = Convert.ToInt32(source.Height * percentWidth); + } + else + { + destinationWidth = Convert.ToInt32(source.Width * percentHeight); + } + + return new Rectangle(0, 0, destinationWidth, destinationHeight); + } + + /// + /// Calculates the target rectangle for min mode. + /// + /// The source image. + /// The resize options. + /// + /// The . + /// + private static Rectangle CalculateMinRectangle(ImageBase source, ResizeOptions options) + { + int width = options.Size.Width; + int height = options.Size.Height; + int destinationWidth; + int destinationHeight; + + // Fractional variants for preserving aspect ratio. + double percentHeight = Math.Abs(height / (double)source.Height); + double percentWidth = Math.Abs(width / (double)source.Width); + + height = height > 0 ? height : Convert.ToInt32(source.Height * percentWidth); + width = width > 0 ? width : Convert.ToInt32(source.Width * percentHeight); + + double sourceRatio = (double)source.Height / source.Width; + + // Find the shortest distance to go. + int widthDiff = source.Width - width; + int heightDiff = source.Height - height; + + if (widthDiff < heightDiff) + { + destinationHeight = Convert.ToInt32(width * sourceRatio); + destinationWidth = width; + } + else if (widthDiff > heightDiff) + { + destinationHeight = height; + destinationWidth = Convert.ToInt32(height / sourceRatio); + } + else + { + destinationWidth = width; + destinationHeight = height; + } + + return new Rectangle(0, 0, destinationWidth, destinationHeight); + } } } diff --git a/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs b/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs index 99fedb33b..ef09530fa 100644 --- a/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs +++ b/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs @@ -182,7 +182,7 @@ { ResizeOptions options = new ResizeOptions() { - Size = new Size(image.Width / 2, image.Height) + Size = new Size(image.Width , image.Height / 2) }; image.Resize(options, this.ProgressUpdate) @@ -192,7 +192,6 @@ Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms"); } } - } [Fact] @@ -215,7 +214,7 @@ { ResizeOptions options = new ResizeOptions() { - Size = new Size(image.Width + 200, image.Height), + Size = new Size(image.Width , image.Height + 200), Mode = ResizeMode.Pad }; @@ -226,7 +225,105 @@ Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms"); } } + } + + [Fact] + public void ImageShouldResizeWithBoxPadMode() + { + if (!Directory.Exists("TestOutput/ResizeBoxPad")) + { + Directory.CreateDirectory("TestOutput/ResizeBoxPad"); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + Stopwatch watch = Stopwatch.StartNew(); + string filename = Path.GetFileName(file); + + using (Image image = new Image(stream)) + using (FileStream output = File.OpenWrite($"TestOutput/ResizeBoxPad/{filename}")) + { + ResizeOptions options = new ResizeOptions() + { + Size = new Size(image.Width + 200, image.Height + 200), + Mode = ResizeMode.BoxPad + }; + + image.Resize(options, this.ProgressUpdate) + .Save(output); + } + + Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms"); + } + } + } + + [Fact] + public void ImageShouldResizeWithMaxMode() + { + if (!Directory.Exists("TestOutput/ResizeMax")) + { + Directory.CreateDirectory("TestOutput/ResizeMax"); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + Stopwatch watch = Stopwatch.StartNew(); + string filename = Path.GetFileName(file); + + using (Image image = new Image(stream)) + using (FileStream output = File.OpenWrite($"TestOutput/ResizeMax/{filename}")) + { + ResizeOptions options = new ResizeOptions() + { + Size = new Size(image.Width + 200, image.Height), + Mode = ResizeMode.Max + }; + + image.Resize(options, this.ProgressUpdate) + .Save(output); + } + + Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms"); + } + } + } + + [Fact] + public void ImageShouldResizeWithMinMode() + { + if (!Directory.Exists("TestOutput/ResizeMin")) + { + Directory.CreateDirectory("TestOutput/ResizeMin"); + } + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + Stopwatch watch = Stopwatch.StartNew(); + string filename = Path.GetFileName(file); + + using (Image image = new Image(stream)) + using (FileStream output = File.OpenWrite($"TestOutput/ResizeMin/{filename}")) + { + ResizeOptions options = new ResizeOptions() + { + Size = new Size(image.Width + 200, image.Height), + Mode = ResizeMode.Min + }; + + image.Resize(options, this.ProgressUpdate) + .Save(output); + } + + Trace.WriteLine($"{filename}: {watch.ElapsedMilliseconds}ms"); + } + } } [Theory]