From 8929cb368f473d37e5cfbbfbd9131a74f542b661 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 31 May 2016 21:00:21 +1000 Subject: [PATCH] Better Rotate, almost there with Skew. Former-commit-id: acc64fb7b84199565b20e48acc02e2bf85f8251b Former-commit-id: 217dd2c5723f414b60b8f46b03876ce9c873fe44 Former-commit-id: f600d8f9917d15984aa601b8ece53ec63af973e2 --- .../Common/Helpers/ImageMaths.cs | 77 ++++----------- src/ImageProcessorCore/Numerics/Point.cs | 7 +- .../Quantizers/Octree/Quantizer.cs | 1 - .../Samplers/ImageSamplerExtensions.cs | 9 +- src/ImageProcessorCore/Samplers/Rotate.cs | 26 ++--- src/ImageProcessorCore/Samplers/Skew.cs | 95 +++++++++++++------ 6 files changed, 103 insertions(+), 112 deletions(-) diff --git a/src/ImageProcessorCore/Common/Helpers/ImageMaths.cs b/src/ImageProcessorCore/Common/Helpers/ImageMaths.cs index ab4394feb..633f7a992 100644 --- a/src/ImageProcessorCore/Common/Helpers/ImageMaths.cs +++ b/src/ImageProcessorCore/Common/Helpers/ImageMaths.cs @@ -13,12 +13,6 @@ namespace ImageProcessorCore /// internal static class ImageMaths { - /// - /// Represents PI, the ratio of a circle's circumference to its diameter. - /// - // ReSharper disable once InconsistentNaming - public const float PI = 3.1415926535897931f; - /// /// Returns how many bits are required to store the specified number of colors. /// Performs a Log2() on the value. @@ -41,7 +35,7 @@ namespace ImageProcessorCore public static float Gaussian(float x, float sigma) { const float Numerator = 1.0f; - float denominator = (float)(Math.Sqrt(2 * PI) * sigma); + float denominator = (float)(Math.Sqrt(2 * Math.PI) * sigma); float exponentNumerator = -x * x; float exponentDenominator = (float)(2 * Math.Pow(sigma, 2)); @@ -90,9 +84,7 @@ namespace ImageProcessorCore /// /// Gets the result of a sine cardinal function for the given value. /// - /// - /// The value to calculate the result for. - /// + /// The value to calculate the result for. /// /// The . /// @@ -102,7 +94,7 @@ namespace ImageProcessorCore if (Math.Abs(x) > Epsilon) { - x *= PI; + x *= (float)Math.PI; return Clean((float)Math.Sin(x) / x); } @@ -118,7 +110,7 @@ namespace ImageProcessorCore /// public static float DegreesToRadians(float degrees) { - return degrees * (PI / 180); + return degrees * (float)(Math.PI / 180); } /// @@ -138,12 +130,16 @@ namespace ImageProcessorCore return new Rectangle(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y); } - // http://gamedev.stackexchange.com/questions/22840/create-a-rectangle-struct-to-be-rotated-and-have-a-intersects-function - public static Rectangle GetBoundingRotatedRectangle(Rectangle rectangle, float degrees, Point center) + /// + /// Gets the bounding from the given matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// + /// The . + /// + public static Rectangle GetBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) { - float radians = DegreesToRadians(degrees); - Matrix3x2 matrix = Matrix3x2.CreateRotation(radians, center.ToVector2()); - Vector2 leftTop = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); Vector2 rightTop = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); Vector2 leftBottom = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); @@ -152,55 +148,16 @@ namespace ImageProcessorCore Vector2 min = Vector2.Min(Vector2.Min(leftTop, rightTop), Vector2.Min(leftBottom, rightBottom)); Vector2 max = Vector2.Max(Vector2.Max(leftTop, rightTop), Vector2.Max(leftBottom, rightBottom)); - // TODO: minY is wrong - negative - return new Rectangle((int)min.X, (int)min.Y, (int)(max.X - min.X), (int)(max.Y - min.Y)); - } - - /// - /// Calculates the new size after rotation. - /// - /// The width of the image. - /// The height of the image. - /// The angle of rotation. - /// The new size of the image - public static Rectangle GetBoundingRotatedRectangle(int width, int height, float angleInDegrees) - { - // Check first clockwise. - double radians = DegreesToRadians(angleInDegrees); - double radiansSin = Math.Sin(radians); - double radiansCos = Math.Cos(radians); - double width1 = (height * radiansSin) + (width * radiansCos); - double height1 = (width * radiansSin) + (height * radiansCos); - - // Find dimensions in the other direction - radiansSin = Math.Sin(-radians); - radiansCos = Math.Cos(-radians); - double width2 = (height * radiansSin) + (width * radiansCos); - double height2 = (width * radiansSin) + (height * radiansCos); - - // Get the external vertex for the rotation - Rectangle result = new Rectangle( - 0, - 0, - Convert.ToInt32(Math.Max(Math.Abs(width1), Math.Abs(width2))), - Convert.ToInt32(Math.Max(Math.Abs(height1), Math.Abs(height2)))); - - return result; + return new Rectangle(0, 0, (int)(max.X - min.X), (int)(max.Y - min.Y)); } /// /// Finds the bounding rectangle based on the first instance of any color component other /// than the given one. /// - /// - /// The to search within. - /// - /// - /// The color component value to remove. - /// - /// - /// The channel to test against. - /// + /// The to search within. + /// The color component value to remove. + /// The channel to test against. /// /// The . /// diff --git a/src/ImageProcessorCore/Numerics/Point.cs b/src/ImageProcessorCore/Numerics/Point.cs index f1fe3c5ec..818002f9e 100644 --- a/src/ImageProcessorCore/Numerics/Point.cs +++ b/src/ImageProcessorCore/Numerics/Point.cs @@ -2,7 +2,6 @@ // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // - namespace ImageProcessorCore { using System; @@ -166,7 +165,7 @@ namespace ImageProcessorCore /// The rotation public static Matrix3x2 CreateRotation(Point origin, float degrees) { - float radians = (float)ImageMaths.DegreesToRadians(degrees); + float radians = ImageMaths.DegreesToRadians(degrees); return Matrix3x2.CreateRotation(radians, origin.backingVector); } @@ -202,8 +201,8 @@ namespace ImageProcessorCore /// The rotation public static Matrix3x2 CreateSkew(Point origin, float degreesX, float degreesY) { - float radiansX = (float)ImageMaths.DegreesToRadians(degreesX); - float radiansY = (float)ImageMaths.DegreesToRadians(degreesY); + float radiansX = ImageMaths.DegreesToRadians(degreesX); + float radiansY = ImageMaths.DegreesToRadians(degreesY); return Matrix3x2.CreateSkew(radiansX, radiansY, origin.backingVector); } diff --git a/src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs b/src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs index 0db212da5..268772cb8 100644 --- a/src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs +++ b/src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs @@ -5,7 +5,6 @@ namespace ImageProcessorCore.Quantizers { - using System; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs b/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs index 45dc3c913..09e059263 100644 --- a/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs +++ b/src/ImageProcessorCore/Samplers/ImageSamplerExtensions.cs @@ -282,7 +282,7 @@ namespace ImageProcessorCore.Samplers } /// - /// Skews an image by the given angles in degrees. + /// Skews an image by the given angles in degrees, expanding the image to fit the skewed result. /// /// The image to skew. /// The angle in degrees to perform the rotation along the x-axis. @@ -291,7 +291,7 @@ namespace ImageProcessorCore.Samplers /// The public static Image Skew(this Image source, float degreesX, float degreesY, ProgressEventHandler progressHandler = null) { - return Skew(source, degreesX, degreesY, Point.Empty, progressHandler); + return Skew(source, degreesX, degreesY, Point.Empty, true, progressHandler); } /// @@ -301,11 +301,12 @@ namespace ImageProcessorCore.Samplers /// The angle in degrees to perform the rotation along the x-axis. /// The angle in degrees to perform the rotation along the y-axis. /// The center point at which to skew the image. + /// Whether to expand the image to fit the skewed result. /// A delegate which is called as progress is made processing the image. /// The - public static Image Skew(this Image source, float degreesX, float degreesY, Point center, ProgressEventHandler progressHandler = null) + public static Image Skew(this Image source, float degreesX, float degreesY, Point center, bool expand, ProgressEventHandler progressHandler = null) { - Skew processor = new Skew { AngleX = degreesX, AngleY = degreesY, Center = center }; + Skew processor = new Skew { AngleX = degreesX, AngleY = degreesY, Center = center, Expand = expand }; processor.OnProgress += progressHandler; try diff --git a/src/ImageProcessorCore/Samplers/Rotate.cs b/src/ImageProcessorCore/Samplers/Rotate.cs index a14b3cee1..e8c823723 100644 --- a/src/ImageProcessorCore/Samplers/Rotate.cs +++ b/src/ImageProcessorCore/Samplers/Rotate.cs @@ -69,9 +69,10 @@ namespace ImageProcessorCore.Samplers // We can use the resizer in nearest neighbor mode to do this fairly quickly. if (this.Expand) { - // First find out how the target rectangle should be. - Rectangle rectangle = ImageMaths.GetBoundingRotatedRectangle(source.Width, source.Height, -this.angle); - Rectangle rectangle2 = ImageMaths.GetBoundingRotatedRectangle(sourceRectangle, -this.angle, this.Center); + // First find out how big the target rectangle should be. + Point centre = this.Center == Point.Empty ? Rectangle.Center(sourceRectangle) : this.Center; + Matrix3x2 rotation = Point.CreateRotation(centre, -this.angle); + Rectangle rectangle = ImageMaths.GetBoundingRectangle(sourceRectangle, rotation); ResizeOptions options = new ResizeOptions { Size = new Size(rectangle.Width, rectangle.Height), @@ -96,30 +97,23 @@ namespace ImageProcessorCore.Samplers /// protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY) { - int targetY = this.firstPass.Bounds.Y; - int targetHeight = this.firstPass.Height; + int height = this.firstPass.Height; int startX = this.firstPass.Bounds.X; int endX = this.firstPass.Bounds.Right; - float negativeAngle = -this.angle; Point centre = this.Center == Point.Empty ? Rectangle.Center(this.firstPass.Bounds) : this.Center; - Matrix3x2 rotation = Point.CreateRotation(centre, negativeAngle); + Matrix3x2 rotation = Point.CreateRotation(centre, -this.angle); - // Since we are not working in parallel we use full height and width of the first pass image. + // Since we are not working in parallel we use full height and width + // of the first pass image. Parallel.For( 0, - targetHeight, + height, y => { - // Y coordinates of source points - int originY = y - targetY; - for (int x = startX; x < endX; x++) { - // X coordinates of source points - int originX = x - startX; - // Rotate at the centre point - Point rotated = Point.Rotate(new Point(originX, originY), rotation); + Point rotated = Point.Rotate(new Point(x, y), rotation); if (this.firstPass.Bounds.Contains(rotated.X, rotated.Y)) { target[x, y] = this.firstPass[rotated.X, rotated.Y]; diff --git a/src/ImageProcessorCore/Samplers/Skew.cs b/src/ImageProcessorCore/Samplers/Skew.cs index 1d88811d1..9248b3016 100644 --- a/src/ImageProcessorCore/Samplers/Skew.cs +++ b/src/ImageProcessorCore/Samplers/Skew.cs @@ -13,6 +13,11 @@ namespace ImageProcessorCore.Samplers /// public class Skew : ImageSampler { + /// + /// The image used for storing the first pass pixels. + /// + private Image firstPass; + /// /// The angle of rotation along the x-axis. /// @@ -23,6 +28,9 @@ namespace ImageProcessorCore.Samplers /// private float angleY; + /// + public override int Parallelism { get; set; } = 1; + /// /// Gets or sets the angle of rotation along the x-axis in degrees. /// @@ -80,44 +88,77 @@ namespace ImageProcessorCore.Samplers /// public Point Center { get; set; } + /// + /// Gets or sets a value indicating whether to expand the canvas to fit the skewed image. + /// + public bool Expand { get; set; } + /// - protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY) + protected override void OnApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle) { - int targetY = targetRectangle.Y; - int targetBottom = targetRectangle.Bottom; - int startX = targetRectangle.X; - int endX = targetRectangle.Right; - float negativeAngleX = -this.angleX; - float negativeAngleY = -this.angleY; - Point centre = this.Center == Point.Empty ? Rectangle.Center(sourceRectangle) : this.Center; - Matrix3x2 skew = Point.CreateSkew(centre, negativeAngleX, negativeAngleY); + // If we are expanding we need to pad the bounds of the source rectangle. + // We can use the resizer in nearest neighbor mode to do this fairly quickly. + if (this.Expand) + { + // First find out how big the target rectangle should be. + Point centre = this.Center == Point.Empty ? Rectangle.Center(sourceRectangle) : this.Center; + Matrix3x2 skew = Point.CreateSkew(centre, -this.angleX, -this.angleY); + Rectangle rectangle = ImageMaths.GetBoundingRectangle(sourceRectangle, skew); + ResizeOptions options = new ResizeOptions + { + Size = new Size(rectangle.Width, rectangle.Height), + Mode = ResizeMode.BoxPad + }; + + // Get the padded bounds and resize the image. + Rectangle bounds = ResizeHelper.CalculateTargetLocationAndBounds(source, options); + this.firstPass = new Image(rectangle.Width, rectangle.Height); + target.SetPixels(rectangle.Width, rectangle.Height, new float[rectangle.Width * rectangle.Height * 4]); + new Resize(new NearestNeighborResampler()).Apply(this.firstPass, source, rectangle.Width, rectangle.Height, bounds, sourceRectangle); + } + else + { + // Just clone the pixels across. + this.firstPass = new Image(source.Width, source.Height); + this.firstPass.ClonePixels(source.Width, source.Height, source.Pixels); + } + } + /// + protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY) + { + int height = this.firstPass.Height; + int startX = this.firstPass.Bounds.X; + int endX = this.firstPass.Bounds.Right; + Point centre = this.Center == Point.Empty ? Rectangle.Center(this.firstPass.Bounds) : this.Center; + Matrix3x2 skew = Point.CreateSkew(centre, -this.angleX, -this.angleY); + + // Since we are not working in parallel we use full height and width + // of the first pass image. Parallel.For( - startY, - endY, + 0, + height, y => { - if (y >= targetY && y < targetBottom) + for (int x = startX; x < endX; x++) { - // Y coordinates of source points - int originY = y - targetY; - - for (int x = startX; x < endX; x++) + // Skew at the centre point + Point skewed = Point.Skew(new Point(x, y), skew); + if (this.firstPass.Bounds.Contains(skewed.X, skewed.Y)) { - // X coordinates of source points - int originX = x - startX; - - // Skew at the centre point - Point skewed = Point.Skew(new Point(originX, originY), skew); - if (sourceRectangle.Contains(skewed.X, skewed.Y)) - { - target[x, y] = source[skewed.X, skewed.Y]; - } + target[x, y] = this.firstPass[skewed.X, skewed.Y]; } - - this.OnRowProcessed(); } + + this.OnRowProcessed(); }); } + + /// + protected override void AfterApply(ImageBase source, ImageBase target, Rectangle targetRectangle, Rectangle sourceRectangle) + { + // Cleanup. + this.firstPass.Dispose(); + } } } \ No newline at end of file