From 7dc0ec8542e9c40512bea9c97f8c2c0dabe57446 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 26 Nov 2015 01:25:02 +1100 Subject: [PATCH] More rotation improvements/tweaks. Former-commit-id: f70a282d078d1fee3858ab7cd10952d5613cfe6c Former-commit-id: 07e613cc7879308b9475ee3a8582a9b5db3ff121 Former-commit-id: 73fdea9a14385d10b74a0abea0ece717170ec960 --- .../Common/Helpers/ImageMaths.cs | 32 +-- src/ImageProcessor/Filters/BackgroundColor.cs | 6 +- src/ImageProcessor/Numerics/Rectangle.cs | 9 +- .../Numerics/RotatedRectangle.cs | 219 ++++++++++++++++++ .../Samplers/ImageSampleExtensions.cs | 11 + src/ImageProcessor/Samplers/Resampler.cs | 126 ++++++---- .../Processors/ProcessorTestBase.cs | 6 +- .../Processors/Samplers/SamplerTests.cs | 4 +- 8 files changed, 343 insertions(+), 70 deletions(-) create mode 100644 src/ImageProcessor/Numerics/RotatedRectangle.cs diff --git a/src/ImageProcessor/Common/Helpers/ImageMaths.cs b/src/ImageProcessor/Common/Helpers/ImageMaths.cs index 7388e6cf5..97a441d06 100644 --- a/src/ImageProcessor/Common/Helpers/ImageMaths.cs +++ b/src/ImageProcessor/Common/Helpers/ImageMaths.cs @@ -6,6 +6,7 @@ namespace ImageProcessor { using System; + using System.Numerics; /// /// Provides common mathematical methods. @@ -113,24 +114,27 @@ namespace ImageProcessor /// Rotates one point around another /// /// - /// The point to rotate. - /// The rotation angle in degrees. - /// The centre point of rotation. If not set the point will equal - /// - /// - /// Rotated point - public static Point RotatePoint(Point pointToRotate, double angleInDegrees, Point? centerPoint = null) + /// The point to rotate. + /// The origin point of rotation. + /// The rotation angle in degrees. + /// + public static Vector2 RotatePoint(Vector2 point, Vector2 origin, float degrees) { - Point center = centerPoint ?? Point.Empty; + double radians = DegreesToRadians(degrees); + double cosTheta = Math.Cos(radians); + double sinTheta = Math.Sin(radians); - double angleInRadians = DegreesToRadians(angleInDegrees); - double cosTheta = Math.Cos(angleInRadians); - double sinTheta = Math.Sin(angleInRadians); - return new Point + Vector2 translatedPoint = new Vector2 { - X = (int)((cosTheta * (pointToRotate.X - center.X)) - (sinTheta * (pointToRotate.Y - center.Y)) + center.X), - Y = (int)((sinTheta * (pointToRotate.X - center.X)) + (cosTheta * (pointToRotate.Y - center.Y)) + center.Y) + X = (float)(origin.X + + (point.X - origin.X) * cosTheta + - (point.Y - origin.Y) * sinTheta), + Y = (float)(origin.Y + + (point.Y - origin.Y) * cosTheta + + (point.X - origin.X) * sinTheta) }; + + return translatedPoint; } /// diff --git a/src/ImageProcessor/Filters/BackgroundColor.cs b/src/ImageProcessor/Filters/BackgroundColor.cs index e1e49d142..4b159b888 100644 --- a/src/ImageProcessor/Filters/BackgroundColor.cs +++ b/src/ImageProcessor/Filters/BackgroundColor.cs @@ -46,11 +46,7 @@ namespace ImageProcessor.Filters { Color color = source[x, y]; - if (color == Color.Empty) - { - color = backgroundColor; - } - else if (color.A < 1) + if (color.A < 1) { color = Color.Lerp(color, backgroundColor, .5f); } diff --git a/src/ImageProcessor/Numerics/Rectangle.cs b/src/ImageProcessor/Numerics/Rectangle.cs index 2be098b51..c7e97b956 100644 --- a/src/ImageProcessor/Numerics/Rectangle.cs +++ b/src/ImageProcessor/Numerics/Rectangle.cs @@ -7,6 +7,7 @@ namespace ImageProcessor { using System; using System.ComponentModel; + using System.Numerics; /// /// Stores a set of four integers that represent the location and size of a rectangle. @@ -150,9 +151,9 @@ namespace ImageProcessor public bool Contains(int x, int y) { return this.X <= x - && x < this.X + this.Width + && x < this.Right && this.Y <= y - && y < this.Y + this.Height; + && y < this.Bottom; } /// @@ -160,9 +161,9 @@ namespace ImageProcessor /// /// The rectangle /// - public static Point Center(Rectangle rectangle) + public static Vector2 Center(Rectangle rectangle) { - return new Point(rectangle.Left + rectangle.Width / 2, rectangle.Top + rectangle.Height / 2); + return new Vector2(rectangle.Left + rectangle.Width / 2, rectangle.Top + rectangle.Height / 2); } /// diff --git a/src/ImageProcessor/Numerics/RotatedRectangle.cs b/src/ImageProcessor/Numerics/RotatedRectangle.cs new file mode 100644 index 000000000..b75cf56a9 --- /dev/null +++ b/src/ImageProcessor/Numerics/RotatedRectangle.cs @@ -0,0 +1,219 @@ +namespace ImageProcessor +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Numerics; + + /// + /// A rotated rectangle. Adapted from the excellent sample. TODO: Refactor into a struct. + /// + /// + public class RotatedRectangle + { + public Rectangle CollisionRectangle; + public float Rotation; + public Vector2 Origin; + + public RotatedRectangle(Rectangle theRectangle, float theInitialRotation) + { + CollisionRectangle = theRectangle; + Rotation = theInitialRotation; + + //Calculate the Rectangles origin. We assume the center of the Rectangle will + //be the point that we will be rotating around and we use that for the origin + Origin = new Vector2((int)theRectangle.Width / 2f, (int)theRectangle.Height / 2f); + } + + /// + /// Used for changing the X and Y position of the RotatedRectangle + /// + /// + /// + public void ChangePosition(int theXPositionAdjustment, int theYPositionAdjustment) + { + CollisionRectangle.X += theXPositionAdjustment; + CollisionRectangle.Y += theYPositionAdjustment; + } + + /// + /// Determines if the specfied point is contained within the rectangular region defined by + /// this . + /// + /// The x-coordinate of the given point. + /// The y-coordinate of the given point. + /// The + public bool Contains(int x, int y) + { + Rectangle rectangle = new Rectangle(x, y, 1, 1); + return this.Intersects(new RotatedRectangle(rectangle, 0.0f)); + } + + /// + /// This intersects method can be used to check a standard XNA framework Rectangle + /// object and see if it collides with a Rotated Rectangle object + /// + /// + /// + public bool Intersects(Rectangle rectangle) + { + return this.Intersects(new RotatedRectangle(rectangle, 0.0f)); + } + + /// + /// Check to see if two Rotated Rectangls have collided + /// + /// + /// + public bool Intersects(RotatedRectangle theRectangle) + { + //Calculate the Axis we will use to determine if a collision has occurred + //Since the objects are rectangles, we only have to generate 4 Axis (2 for + //each rectangle) since we know the other 2 on a rectangle are parallel. + List aRectangleAxis = new List + { + this.UpperRightCorner() - this.UpperLeftCorner(), + this.UpperRightCorner() - this.LowerRightCorner(), + theRectangle.UpperLeftCorner() - theRectangle.LowerLeftCorner(), + theRectangle.UpperLeftCorner() - theRectangle.UpperRightCorner() + }; + + //Cycle through all of the Axis we need to check. If a collision does not occur + //on ALL of the Axis, then a collision is NOT occurring. We can then exit out + //immediately and notify the calling function that no collision was detected. If + //a collision DOES occur on ALL of the Axis, then there is a collision occurring + //between the rotated rectangles. We know this to be true by the Seperating Axis Theorem + return aRectangleAxis.All(aAxis => this.IsAxisCollision(theRectangle, aAxis)); + } + + /// + /// Determines if a collision has occurred on an Axis of one of the + /// planes parallel to the Rectangle + /// + /// + /// + /// + private bool IsAxisCollision(RotatedRectangle theRectangle, Vector2 aAxis) + { + //Project the corners of the Rectangle we are checking on to the Axis and + //get a scalar value of that project we can then use for comparison + List aRectangleAScalars = new List + { + this.GenerateScalar(theRectangle.UpperLeftCorner(), aAxis), + this.GenerateScalar(theRectangle.UpperRightCorner(), aAxis), + this.GenerateScalar(theRectangle.LowerLeftCorner(), aAxis), + this.GenerateScalar(theRectangle.LowerRightCorner(), aAxis) + }; + + //Project the corners of the current Rectangle on to the Axis and + //get a scalar value of that project we can then use for comparison + List aRectangleBScalars = new List + { + this.GenerateScalar(this.UpperLeftCorner(), aAxis), + this.GenerateScalar(this.UpperRightCorner(), aAxis), + this.GenerateScalar(this.LowerLeftCorner(), aAxis), + this.GenerateScalar(this.LowerRightCorner(), aAxis) + }; + + //Get the Maximum and Minium Scalar values for each of the Rectangles + int aRectangleAMinimum = aRectangleAScalars.Min(); + int aRectangleAMaximum = aRectangleAScalars.Max(); + int aRectangleBMinimum = aRectangleBScalars.Min(); + int aRectangleBMaximum = aRectangleBScalars.Max(); + + //If we have overlaps between the Rectangles (i.e. Min of B is less than Max of A) + //then we are detecting a collision between the rectangles on this Axis + if (aRectangleBMinimum <= aRectangleAMaximum && aRectangleBMaximum >= aRectangleAMaximum) + { + return true; + } + else if (aRectangleAMinimum <= aRectangleBMaximum && aRectangleAMaximum >= aRectangleBMaximum) + { + return true; + } + + return false; + } + + /// + /// Generates a scalar value that can be used to compare where corners of + /// a rectangle have been projected onto a particular axis. + /// + /// + /// + /// + private int GenerateScalar(Vector2 corner, Vector2 axis) + { + // Using the formula for Vector projection. Take the corner being passed in + // and project it onto the given Axis + float numerator = Vector2.Dot(corner, axis); //(theRectangleCorner.X * theAxis.X) + (theRectangleCorner.Y * theAxis.Y); + float denominator = Vector2.Dot(axis, axis); //(theAxis.X * theAxis.X) + (theAxis.Y * theAxis.Y); + float aDivisionResult = numerator / denominator; + + Vector2 projected = new Vector2(aDivisionResult * axis.X, aDivisionResult * axis.Y); + + // Now that we have our projected Vector, calculate a scalar of that projection + // that can be used to more easily do comparisons + float scalar = Vector2.Dot(axis, projected); + return (int)scalar; + } + + /// + /// Rotate a point from a given location and adjust using the Origin we + /// are rotating around + /// + /// + /// + /// + /// + private Vector2 RotatePoint(Vector2 thePoint, Vector2 theOrigin, float theRotation) + { + Vector2 aTranslatedPoint = new Vector2 + { + X = (float)(theOrigin.X + + (thePoint.X - theOrigin.X) * Math.Cos(theRotation) + - (thePoint.Y - theOrigin.Y) * Math.Sin(theRotation)), + Y = (float)(theOrigin.Y + + (thePoint.Y - theOrigin.Y) * Math.Cos(theRotation) + + (thePoint.X - theOrigin.X) * Math.Sin(theRotation)) + }; + return aTranslatedPoint; + } + + public Vector2 UpperLeftCorner() + { + Vector2 aUpperLeft = new Vector2(this.CollisionRectangle.Left, this.CollisionRectangle.Top); + aUpperLeft = this.RotatePoint(aUpperLeft, aUpperLeft + this.Origin, this.Rotation); + return aUpperLeft; + } + + public Vector2 UpperRightCorner() + { + Vector2 aUpperRight = new Vector2(this.CollisionRectangle.Right, this.CollisionRectangle.Top); + aUpperRight = this.RotatePoint(aUpperRight, aUpperRight + new Vector2(-this.Origin.X, this.Origin.Y), this.Rotation); + return aUpperRight; + } + + public Vector2 LowerLeftCorner() + { + Vector2 aLowerLeft = new Vector2(this.CollisionRectangle.Left, this.CollisionRectangle.Bottom); + aLowerLeft = this.RotatePoint(aLowerLeft, aLowerLeft + new Vector2(this.Origin.X, -this.Origin.Y), this.Rotation); + return aLowerLeft; + } + + public Vector2 LowerRightCorner() + { + Vector2 aLowerRight = new Vector2(this.CollisionRectangle.Right, this.CollisionRectangle.Bottom); + aLowerRight = this.RotatePoint(aLowerRight, aLowerRight + new Vector2(-this.Origin.X, -this.Origin.Y), this.Rotation); + return aLowerRight; + } + + public int X => this.CollisionRectangle.X; + + public int Y => this.CollisionRectangle.Y; + + public int Width => this.CollisionRectangle.Width; + + public int Height => this.CollisionRectangle.Height; + } +} diff --git a/src/ImageProcessor/Samplers/ImageSampleExtensions.cs b/src/ImageProcessor/Samplers/ImageSampleExtensions.cs index a73eb2b2c..96e0b3c5f 100644 --- a/src/ImageProcessor/Samplers/ImageSampleExtensions.cs +++ b/src/ImageProcessor/Samplers/ImageSampleExtensions.cs @@ -52,6 +52,17 @@ namespace ImageProcessor.Samplers return source.Process(width, height, sourceRectangle, new Rectangle(0, 0, width, height), new Resampler(sampler)); } + /// + /// Rotates an image by the given angle in degrees. + /// + /// The image to resize. + /// The angle in degrees to porform the rotation. + /// The + public static Image Rotate(this Image source, float degrees) + { + return source.Process(source.Width, source.Height, source.Bounds, source.Bounds, new Resampler(new RobidouxResampler()) { Angle = degrees }); + } + /// /// Crops an image to the given width and height. /// diff --git a/src/ImageProcessor/Samplers/Resampler.cs b/src/ImageProcessor/Samplers/Resampler.cs index a98ac835e..401e4f451 100644 --- a/src/ImageProcessor/Samplers/Resampler.cs +++ b/src/ImageProcessor/Samplers/Resampler.cs @@ -7,6 +7,7 @@ namespace ImageProcessor.Samplers { using System; using System.Collections.Immutable; + using System.Numerics; using System.Threading.Tasks; /// @@ -14,15 +15,10 @@ namespace ImageProcessor.Samplers /// public class Resampler : ParallelImageProcessor { - /// - /// The epsilon for comparing floating point numbers. - /// - private const float Epsilon = 0.000001f; - /// /// The angle of rotation. /// - private double angle; + private float angle; /// /// The horizontal weights. @@ -55,7 +51,7 @@ namespace ImageProcessor.Samplers /// /// Gets or sets the angle of rotation. /// - public double Angle + public float Angle { get { @@ -87,13 +83,26 @@ namespace ImageProcessor.Samplers /// 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); + } + } + + private void ApplyResizeOnly(ImageBase target, ImageBase source, Rectangle targetRectangle, int startY, int endY) { int targetY = targetRectangle.Y; int targetBottom = targetRectangle.Bottom; int startX = targetRectangle.X; int endX = targetRectangle.Right; - Point centre = Rectangle.Center(sourceRectangle); - bool rotate = this.angle > 0 && this.angle < 360; Parallel.For( startY, @@ -120,32 +129,71 @@ namespace ImageProcessor.Samplers foreach (Weight xw in horizontalValues) { int originX = xw.Index; - Color sourceColor; + Color sourceColor = Color.InverseCompand(source[originX, originY]); + float weight = (yw.Value / verticalSum) * (xw.Value / horizontalSum); - float weight; + destination.R += sourceColor.R * weight; + destination.G += sourceColor.G * weight; + destination.B += sourceColor.B * weight; + destination.A += sourceColor.A * weight; + } + } - if (rotate) - { - // Rotating at the centre point - Point rotated = ImageMaths.RotatePoint(new Point(originX, originY), this.angle, centre); - int rotatedX = rotated.X; - int rotatedY = rotated.Y; - - if (sourceRectangle.Contains(rotatedX, rotatedY)) - { - sourceColor = Color.InverseCompand(source[rotatedX, rotatedY]); - weight = (yw.Value / verticalSum) * (xw.Value / horizontalSum); - - destination.R += sourceColor.R * weight; - destination.G += sourceColor.G * weight; - destination.B += sourceColor.B * weight; - destination.A += sourceColor.A * weight; - } - } - else + destination = Color.Compand(destination); + + // Round alpha values in an attempt to prevent bleed. + destination.A = (float)Math.Round(destination.A, 2); + + target[x, y] = destination; + } + } + }); + } + + 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; + Vector2 centre = Rectangle.Center(sourceRectangle); + + Parallel.For( + startY, + endY, + y => + { + if (y >= targetY && y < targetBottom) + { + ImmutableArray verticalValues = this.verticalWeights[y].Values; + float verticalSum = this.verticalWeights[y].Sum; + + for (int x = startX; x < endX; x++) + { + ImmutableArray horizontalValues = this.horizontalWeights[x].Values; + float horizontalSum = this.horizontalWeights[x].Sum; + + // Destination color components + Color destination = new Color(0, 0, 0, 0); + + foreach (Weight yw in verticalValues) + { + int originY = yw.Index; + + foreach (Weight xw in horizontalValues) + { + int originX = xw.Index; + + // Rotate at the centre point + Vector2 rotated = ImageMaths.RotatePoint(new Vector2(originX, originY), centre, negativeAngle); + int rotatedX = (int)rotated.X; + int rotatedY = (int)rotated.Y; + + if (sourceRectangle.Contains(rotatedX, rotatedY)) { - sourceColor = Color.InverseCompand(source[originX, originY]); - weight = (yw.Value / verticalSum) * (xw.Value / horizontalSum); + Color sourceColor = Color.InverseCompand(source[rotatedX, rotatedY]); + float weight = (yw.Value / verticalSum) * (xw.Value / horizontalSum); destination.R += sourceColor.R * weight; destination.G += sourceColor.G * weight; @@ -155,18 +203,10 @@ namespace ImageProcessor.Samplers } } - // Restrict alpha values in an attempt to prevent bleed. - // This is a baaaaaaad hack!!! - //if (destination.A <= 0.03) - //{ - // destination = Color.Empty; - //} - //else - //{ - destination = Color.Compand(destination); - destination.A = (float)Math.Round(destination.A, 2); - //} + destination = Color.Compand(destination); + // Round alpha values in an attempt to prevent bleed. + destination.A = (float)Math.Round(destination.A, 2); target[x, y] = destination; } } diff --git a/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs b/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs index 28c92d66e..f901c441d 100644 --- a/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs +++ b/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs @@ -28,13 +28,13 @@ namespace ImageProcessor.Tests //"TestImages/Formats/Jpg/shaftesbury.jpg", //"TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg", //"TestImages/Formats/Jpg/greyscale.jpg", - "TestImages/Formats/Bmp/Car.bmp", - "TestImages/Formats/Png/cballs.png", + //"TestImages/Formats/Bmp/Car.bmp", + //"TestImages/Formats/Png/cballs.png", //"TestImages/Formats/Png/blur.png", //"TestImages/Formats/Png/cmyk.png", //"TestImages/Formats/Png/gamma-1.0-or-2.2.png", //"TestImages/Formats/Png/splash.png", - "TestImages/Formats/Gif/leaf.gif", + //"TestImages/Formats/Gif/leaf.gif", //"TestImages/Formats/Gif/ben2.gif", //"TestImages/Formats/Gif/rings.gif", //"TestImages/Formats/Gif/ani2.gif", diff --git a/tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs b/tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs index e171341f8..7c8b6e36b 100644 --- a/tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs +++ b/tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs @@ -7,6 +7,7 @@ namespace ImageProcessor.Tests using ImageProcessor.Samplers; using Xunit; + using Filters; public class SamplerTests : ProcessorTestBase { @@ -46,7 +47,8 @@ namespace ImageProcessor.Tests string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); using (FileStream output = File.OpenWrite($"TestOutput/Resized/{filename}")) { - image.Resize(image.Width / 2, image.Height / 2, sampler).Save(output); + image.Resize(image.Width / 2, image.Height / 2, sampler) + .Save(output); } Trace.WriteLine($"{name}: {watch.ElapsedMilliseconds}ms");