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");