Browse Source

More rotation improvements/tweaks.

Former-commit-id: f70a282d078d1fee3858ab7cd10952d5613cfe6c
Former-commit-id: 07e613cc7879308b9475ee3a8582a9b5db3ff121
Former-commit-id: 73fdea9a14385d10b74a0abea0ece717170ec960
af/merge-core
James Jackson-South 10 years ago
parent
commit
7dc0ec8542
  1. 32
      src/ImageProcessor/Common/Helpers/ImageMaths.cs
  2. 6
      src/ImageProcessor/Filters/BackgroundColor.cs
  3. 9
      src/ImageProcessor/Numerics/Rectangle.cs
  4. 219
      src/ImageProcessor/Numerics/RotatedRectangle.cs
  5. 11
      src/ImageProcessor/Samplers/ImageSampleExtensions.cs
  6. 126
      src/ImageProcessor/Samplers/Resampler.cs
  7. 6
      tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs
  8. 4
      tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs

32
src/ImageProcessor/Common/Helpers/ImageMaths.cs

@ -6,6 +6,7 @@
namespace ImageProcessor
{
using System;
using System.Numerics;
/// <summary>
/// Provides common mathematical methods.
@ -113,24 +114,27 @@ namespace ImageProcessor
/// Rotates one point around another
/// <see href="http://stackoverflow.com/a/13695630/82333"/>
/// </summary>
/// <param name="pointToRotate">The point to rotate.</param>
/// <param name="angleInDegrees">The rotation angle in degrees.</param>
/// <param name="centerPoint">The centre point of rotation. If not set the point will equal
/// <see cref="Point.Empty"/>
/// </param>
/// <returns>Rotated point</returns>
public static Point RotatePoint(Point pointToRotate, double angleInDegrees, Point? centerPoint = null)
/// <param name="point">The point to rotate.</param>
/// <param name="origin">The origin point of rotation.</param>
/// <param name="degrees">The rotation angle in degrees.</param>
/// <returns><see cref="Vector2"/></returns>
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;
}
/// <summary>

6
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);
}

9
src/ImageProcessor/Numerics/Rectangle.cs

@ -7,6 +7,7 @@ namespace ImageProcessor
{
using System;
using System.ComponentModel;
using System.Numerics;
/// <summary>
/// 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;
}
/// <summary>
@ -160,9 +161,9 @@ namespace ImageProcessor
/// </summary>
/// <param name="rectangle">The rectangle</param>
/// <returns><see cref="Point"/></returns>
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);
}
/// <summary>

219
src/ImageProcessor/Numerics/RotatedRectangle.cs

@ -0,0 +1,219 @@
namespace ImageProcessor
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
/// <summary>
/// A rotated rectangle. Adapted from the excellent sample. TODO: Refactor into a struct.
/// <see href="http://www.xnadevelopment.com/tutorials/rotatedrectanglecollisions/rotatedrectanglecollisions.shtml"/>
/// </summary>
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);
}
/// <summary>
/// Used for changing the X and Y position of the RotatedRectangle
/// </summary>
/// <param name="theXPositionAdjustment"></param>
/// <param name="theYPositionAdjustment"></param>
public void ChangePosition(int theXPositionAdjustment, int theYPositionAdjustment)
{
CollisionRectangle.X += theXPositionAdjustment;
CollisionRectangle.Y += theYPositionAdjustment;
}
/// <summary>
/// Determines if the specfied point is contained within the rectangular region defined by
/// this <see cref="RotatedRectangle"/>.
/// </summary>
/// <param name="x">The x-coordinate of the given point.</param>
/// <param name="y">The y-coordinate of the given point.</param>
/// <returns>The <see cref="bool"/></returns>
public bool Contains(int x, int y)
{
Rectangle rectangle = new Rectangle(x, y, 1, 1);
return this.Intersects(new RotatedRectangle(rectangle, 0.0f));
}
/// <summary>
/// This intersects method can be used to check a standard XNA framework Rectangle
/// object and see if it collides with a Rotated Rectangle object
/// </summary>
/// <param name="rectangle"></param>
/// <returns></returns>
public bool Intersects(Rectangle rectangle)
{
return this.Intersects(new RotatedRectangle(rectangle, 0.0f));
}
/// <summary>
/// Check to see if two Rotated Rectangls have collided
/// </summary>
/// <param name="theRectangle"></param>
/// <returns></returns>
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<Vector2> aRectangleAxis = new List<Vector2>
{
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));
}
/// <summary>
/// Determines if a collision has occurred on an Axis of one of the
/// planes parallel to the Rectangle
/// </summary>
/// <param name="theRectangle"></param>
/// <param name="aAxis"></param>
/// <returns></returns>
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<int> aRectangleAScalars = new List<int>
{
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<int> aRectangleBScalars = new List<int>
{
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;
}
/// <summary>
/// Generates a scalar value that can be used to compare where corners of
/// a rectangle have been projected onto a particular axis.
/// </summary>
/// <param name="corner"></param>
/// <param name="axis"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Rotate a point from a given location and adjust using the Origin we
/// are rotating around
/// </summary>
/// <param name="thePoint"></param>
/// <param name="theOrigin"></param>
/// <param name="theRotation"></param>
/// <returns></returns>
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;
}
}

11
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));
}
/// <summary>
/// Rotates an image by the given angle in degrees.
/// </summary>
/// <param name="source">The image to resize.</param>
/// <param name="degrees">The angle in degrees to porform the rotation.</param>
/// <returns>The <see cref="Image"/></returns>
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 });
}
/// <summary>
/// Crops an image to the given width and height.
/// </summary>

126
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;
/// <summary>
@ -14,15 +15,10 @@ namespace ImageProcessor.Samplers
/// </summary>
public class Resampler : ParallelImageProcessor
{
/// <summary>
/// The epsilon for comparing floating point numbers.
/// </summary>
private const float Epsilon = 0.000001f;
/// <summary>
/// The angle of rotation.
/// </summary>
private double angle;
private float angle;
/// <summary>
/// The horizontal weights.
@ -55,7 +51,7 @@ namespace ImageProcessor.Samplers
/// <summary>
/// Gets or sets the angle of rotation.
/// </summary>
public double Angle
public float Angle
{
get
{
@ -87,13 +83,26 @@ namespace ImageProcessor.Samplers
/// <inheritdoc/>
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<Weight> verticalValues = this.verticalWeights[y].Values;
float verticalSum = this.verticalWeights[y].Sum;
for (int x = startX; x < endX; x++)
{
ImmutableArray<Weight> 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;
}
}

6
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",

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

Loading…
Cancel
Save