Browse Source

Merge pull request #41 from tocsoft/optimise-polygon-merging

Optimise polygon merging in complex polygons
af/merge-core
James Jackson-South 9 years ago
committed by GitHub
parent
commit
50abd9f31f
  1. 24
      src/ImageSharp/Drawing/Paths/InternalPath.cs
  2. 187
      src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs
  3. 13
      src/ImageSharp/Drawing/Shapes/Polygon.cs
  4. 49
      tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs

24
src/ImageSharp/Drawing/Paths/InternalPath.cs

@ -60,10 +60,28 @@ namespace ImageSharp.Drawing.Paths
/// <param name="segments">The segments.</param>
/// <param name="isClosedPath">if set to <c>true</c> [is closed path].</param>
internal InternalPath(ILineSegment[] segments, bool isClosedPath)
: this(Simplify(segments), isClosedPath)
{
Guard.NotNull(segments, nameof(segments));
}
this.points = this.Simplify(segments);
/// <summary>
/// Initializes a new instance of the <see cref="InternalPath" /> class.
/// </summary>
/// <param name="segment">The segment.</param>
/// <param name="isClosedPath">if set to <c>true</c> [is closed path].</param>
internal InternalPath(ILineSegment segment, bool isClosedPath)
: this(segment.AsSimpleLinearPath(), isClosedPath)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InternalPath" /> class.
/// </summary>
/// <param name="points">The points.</param>
/// <param name="isClosedPath">if set to <c>true</c> [is closed path].</param>
internal InternalPath(Vector2[] points, bool isClosedPath)
{
this.points = points;
this.closedPath = isClosedPath;
float minX = this.points.Min(x => x.X);
@ -192,7 +210,7 @@ namespace ImageSharp.Drawing.Paths
/// <returns>
/// The <see cref="T:Vector2[]"/>.
/// </returns>
private Vector2[] Simplify(ILineSegment[] segments)
private static Vector2[] Simplify(ILineSegment[] segments)
{
List<Vector2> simplified = new List<Vector2>();
foreach (ILineSegment seg in segments)

187
src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs

@ -20,8 +20,7 @@ namespace ImageSharp.Drawing.Shapes
public sealed class ComplexPolygon : IShape
{
private const float ClipperScaleFactor = 100f;
private IEnumerable<IShape> holes;
private IEnumerable<IShape> outlines;
private IShape[] shapes;
private IEnumerable<IPath> paths;
/// <summary>
@ -39,17 +38,17 @@ namespace ImageSharp.Drawing.Shapes
/// </summary>
/// <param name="outlines">The outlines.</param>
/// <param name="holes">The holes.</param>
public ComplexPolygon(IEnumerable<IShape> outlines, IEnumerable<IShape> holes)
public ComplexPolygon(IShape[] outlines, IShape[] holes)
{
Guard.NotNull(outlines, nameof(outlines));
Guard.MustBeGreaterThanOrEqualTo(outlines.Count(), 1, nameof(outlines));
Guard.MustBeGreaterThanOrEqualTo(outlines.Length, 1, nameof(outlines));
this.FixAndSetShapes(outlines, holes);
var minX = outlines.Min(x => x.Bounds.Left);
var maxX = outlines.Max(x => x.Bounds.Right);
var minY = outlines.Min(x => x.Bounds.Top);
var maxY = outlines.Max(x => x.Bounds.Bottom);
var minX = this.shapes.Min(x => x.Bounds.Left);
var maxX = this.shapes.Max(x => x.Bounds.Right);
var minY = this.shapes.Min(x => x.Bounds.Top);
var maxY = this.shapes.Max(x => x.Bounds.Bottom);
this.Bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY);
}
@ -69,30 +68,37 @@ namespace ImageSharp.Drawing.Shapes
/// <returns>
/// Returns the distance from thr shape to the point
/// </returns>
/// <remarks>
/// Due to the clipping we did during construction we know that out shapes do not overlap at there edges
/// therefore for apoint to be in more that one we must be in a hole of another, theoretically this could
/// then flip again to be in a outlin inside a hole inside an outline :)
/// </remarks>
float IShape.Distance(Vector2 point)
{
// get the outline we are closest to the center of
// by rights we should only be inside 1 outline
// othersie we will start returning the distanct to the nearest shape
var dist = this.outlines.Select(o => o.Distance(point)).OrderBy(p => p).First();
if (dist <= 0)
float dist = float.MaxValue;
bool inside = false;
foreach (IShape shape in this.shapes)
{
// inside poly
foreach (var hole in this.holes)
var d = shape.Distance(point);
if (d <= 0)
{
var distFromHole = hole.Distance(point);
// we are inside a poly
d = -d; // flip the sign
inside ^= true; // flip the inside flag
}
// less than zero we are inside shape
if (distFromHole <= 0)
{
// invert distance
dist = distFromHole * -1;
break;
}
if (d < dist)
{
dist = d;
}
}
if (inside)
{
return -dist;
}
return dist;
}
@ -138,59 +144,136 @@ namespace ImageSharp.Drawing.Shapes
}
}
private void AddPoints(ClipperLib.Clipper clipper, IEnumerable<IShape> shapes, ClipperLib.PolyType polyType)
private void AddPoints(ClipperLib.Clipper clipper, IShape[] shapes, bool[] shouldInclude, ClipperLib.PolyType polyType)
{
foreach (var shape in shapes)
for (var i = 0; i < shapes.Length; i++)
{
this.AddPoints(clipper, shape, polyType);
if (shouldInclude[i])
{
this.AddPoints(clipper, shapes[i], polyType);
}
}
}
private void ExtractOutlines(ClipperLib.PolyNode tree, List<Polygon> outlines, List<Polygon> holes)
private void ExtractOutlines(ClipperLib.PolyNode tree, List<IShape> shapes)
{
if (tree.Contour.Any())
{
// convert the Clipper Contour from scaled ints back down to the origional size (this is going to be lossy but not significantly)
var polygon = new Polygon(new LinearLineSegment(tree.Contour.Select(x => new Vector2(x.X / ClipperScaleFactor, x.Y / ClipperScaleFactor)).ToArray()));
if (tree.IsHole)
var pointCount = tree.Contour.Count;
var vectors = new Vector2[pointCount];
for (var i = 0; i < pointCount; i++)
{
holes.Add(polygon);
}
else
{
outlines.Add(polygon);
var p = tree.Contour[i];
vectors[i] = new Vector2(p.X, p.Y) / ClipperScaleFactor;
}
var polygon = new Polygon(new LinearLineSegment(vectors));
shapes.Add(polygon);
}
foreach (var c in tree.Childs)
{
this.ExtractOutlines(c, outlines, holes);
this.ExtractOutlines(c, shapes);
}
}
private void FixAndSetShapes(IEnumerable<IShape> outlines, IEnumerable<IShape> holes)
/// <summary>
/// Determines if the <see cref="IShape"/>s bounding boxes overlap.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="target">The target.</param>
/// <returns>true if the 2 shapes bounding boxes overlap.</returns>
private bool OverlappingBoundingBoxes(IShape source, IShape target)
{
return source.Bounds.Intersects(target.Bounds);
}
private void FixAndSetShapes(IShape[] outlines, IShape[] holes)
{
var clipper = new ClipperLib.Clipper();
// if any outline doesn't overlap another shape then we don't have to bother with sending them through clipper
// as sending then though clipper will turn them into generic polygons and loose thier shape specific optimisations
int outlineLength = outlines.Length;
int holesLength = holes?.Length ?? 0;
bool[] overlappingOutlines = new bool[outlineLength];
bool[] overlappingHoles = new bool[holesLength];
bool anyOutlinesOverlapping = false;
bool anyHolesOverlapping = false;
for (int i = 0; i < outlineLength; i++)
{
for (int j = i + 1; j < outlineLength; j++)
{
// skip the bounds check if they are already tested
if (overlappingOutlines[i] == false || overlappingOutlines[j] == false)
{
if (this.OverlappingBoundingBoxes(outlines[i], outlines[j]))
{
overlappingOutlines[i] = true;
overlappingOutlines[j] = true;
anyOutlinesOverlapping = true;
}
}
}
for (int k = 0; k < holesLength; k++)
{
if (overlappingOutlines[i] == false || overlappingHoles[k] == false)
{
if (this.OverlappingBoundingBoxes(outlines[i], holes[k]))
{
overlappingOutlines[i] = true;
overlappingHoles[k] = true;
anyOutlinesOverlapping = true;
anyHolesOverlapping = true;
}
}
}
}
if (anyOutlinesOverlapping)
{
var clipper = new ClipperLib.Clipper();
// add the outlines and the holes to clipper, scaling up from the float source to the int based system clipper uses
this.AddPoints(clipper, outlines, overlappingOutlines, ClipperLib.PolyType.ptSubject);
if (anyHolesOverlapping)
{
this.AddPoints(clipper, holes, overlappingHoles, ClipperLib.PolyType.ptClip);
}
var tree = new ClipperLib.PolyTree();
clipper.Execute(ClipperLib.ClipType.ctDifference, tree);
// add the outlines and the holes to clipper, scaling up from the float source to the int based system clipper uses
this.AddPoints(clipper, outlines, ClipperLib.PolyType.ptSubject);
this.AddPoints(clipper, holes, ClipperLib.PolyType.ptClip);
List<IShape> newShapes = new List<IShape>();
var tree = new ClipperLib.PolyTree();
clipper.Execute(ClipperLib.ClipType.ctDifference, tree);
// convert the 'tree' back to shapes
this.ExtractOutlines(tree, newShapes);
List<Polygon> newOutlines = new List<Polygon>();
List<Polygon> newHoles = new List<Polygon>();
// add the origional outlines that where not overlapping
for (int i = 0; i < outlineLength - 1; i++)
{
if (!overlappingOutlines[i])
{
newShapes.Add(outlines[i]);
}
}
// convert the 'tree' back to paths
this.ExtractOutlines(tree, newOutlines, newHoles);
this.shapes = newShapes.ToArray();
}
else
{
this.shapes = outlines;
}
this.outlines = newOutlines;
this.holes = newHoles;
var paths = new List<IPath>();
foreach (var o in this.shapes)
{
paths.AddRange(o);
}
// extract the final list of paths out of the new polygons we just converted down to.
this.paths = newOutlines.Union(newHoles).ToArray();
this.paths = paths;
}
}
}

13
src/ImageSharp/Drawing/Shapes/Polygon.cs

@ -29,6 +29,16 @@ namespace ImageSharp.Drawing.Shapes
this.pathCollection = new[] { this };
}
/// <summary>
/// Initializes a new instance of the <see cref="Polygon" /> class.
/// </summary>
/// <param name="segment">The segment.</param>
public Polygon(ILineSegment segment)
{
this.innerPath = new InternalPath(segment, true);
this.pathCollection = new[] { this };
}
/// <summary>
/// Gets the bounding box of this shape.
/// </summary>
@ -98,8 +108,7 @@ namespace ImageSharp.Drawing.Shapes
/// <summary>
/// Calcualtes the distance along and away from the path for a specified point.
/// </summary>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <param name="point">The point along the path.</param>
/// <returns>
/// distance metadata about the point.
/// </returns>

49
tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs

@ -66,6 +66,55 @@ namespace ImageSharp.Tests.Drawing
}
}
[Fact]
public void ImageShouldBeOverlayedByPolygonOutlineNoOverlapping()
{
string path = CreateOutputDirectory("Drawing", "LineComplexPolygon");
var simplePath = new LinearPolygon(
new Vector2(10, 10),
new Vector2(200, 150),
new Vector2(50, 300));
var hole1 = new LinearPolygon(
new Vector2(207, 25),
new Vector2(263, 25),
new Vector2(235, 57));
var image = new Image(500, 500);
using (FileStream output = File.OpenWrite($"{path}/SimpleVanishHole.png"))
{
image
.BackgroundColor(Color.Blue)
.DrawPolygon(Color.HotPink, 5, new ComplexPolygon(simplePath, hole1))
.Save(output);
}
using (var sourcePixels = image.Lock())
{
Assert.Equal(Color.HotPink, sourcePixels[10, 10]);
Assert.Equal(Color.HotPink, sourcePixels[200, 150]);
Assert.Equal(Color.HotPink, sourcePixels[50, 300]);
//Assert.Equal(Color.HotPink, sourcePixels[37, 85]);
//Assert.Equal(Color.HotPink, sourcePixels[93, 85]);
//Assert.Equal(Color.HotPink, sourcePixels[65, 137]);
Assert.Equal(Color.Blue, sourcePixels[2, 2]);
//inside hole
Assert.Equal(Color.Blue, sourcePixels[57, 99]);
//inside shape
Assert.Equal(Color.Blue, sourcePixels[100, 192]);
}
}
[Fact]
public void ImageShouldBeOverlayedByPolygonOutlineOverlapping()

Loading…
Cancel
Save