Browse Source

optimise complex polygons

Trying to avoid using clipper to merge polygons when they can be avoided.
Try to use the origional (potentially optermised shapes) if merging will produce not change the origional shape.

We do this in a couple of ways:
- We strip away any holes that don't intersect with an outline
- We skip merging outlines that dont intersect with any other shape and simple pass then through. This provides the added benefit/optimisation of using the specialised shapes instead of the generic polygon class where possible.
af/merge-core
Scott Williams 10 years ago
parent
commit
691c214a11
  1. 25
      src/ImageSharp/Drawing/Paths/InternalPath.cs
  2. 146
      src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs
  3. 10
      src/ImageSharp/Drawing/Shapes/Polygon.cs
  4. 49
      tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs

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

@ -60,10 +60,29 @@ 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));
}
/// <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)
{
}
this.points = this.Simplify(segments);
/// <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 +211,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)

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

@ -7,7 +7,6 @@ namespace ImageSharp.Drawing.Shapes
{
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Paths;
@ -20,8 +19,8 @@ namespace ImageSharp.Drawing.Shapes
public sealed class ComplexPolygon : IShape
{
private const float ClipperScaleFactor = 100f;
private IEnumerable<IShape> holes;
private IEnumerable<IShape> outlines;
private IShape[] holes;
private IShape[] outlines;
private IEnumerable<IPath> paths;
/// <summary>
@ -39,7 +38,7 @@ 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));
@ -76,7 +75,7 @@ namespace ImageSharp.Drawing.Shapes
// 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)
if (dist <= 0 && this.holes != null)
{
// inside poly
foreach (var hole in this.holes)
@ -138,20 +137,32 @@ 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> outlines, List<IShape> holes)
{
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()));
var pointCount = tree.Contour.Count;
var vectors = new Vector2[pointCount];
for(var i=0; i< pointCount; i++)
{
var p = tree.Contour[i];
vectors[i] = new Vector2(p.X, p.Y) / ClipperScaleFactor;
}
var polygon = new Polygon(new LinearLineSegment(vectors));
if (tree.IsHole)
{
@ -169,28 +180,113 @@ namespace ImageSharp.Drawing.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></returns>
private bool OverlappingBoundingBoxes(IShape source, IShape target)
{
var clipper = new ClipperLib.Clipper();
return source.Bounds.Intersects(target.Bounds);
}
// 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);
var tree = new ClipperLib.PolyTree();
clipper.Execute(ClipperLib.ClipType.ctDifference, tree);
private void FixAndSetShapes(IShape[] outlines, IShape[] holes)
{
// 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
List<Polygon> newOutlines = new List<Polygon>();
List<Polygon> newHoles = new List<Polygon>();
int outlineLength = outlines.Length;
int holesLength = holes.Length;
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 (OverlappingBoundingBoxes(outlines[i], outlines[j]))
{
overlappingOutlines[i] = true;
overlappingOutlines[j] = true;
anyOutlinesOverlapping = true;
}
}
}
// convert the 'tree' back to paths
this.ExtractOutlines(tree, newOutlines, newHoles);
for (int k = 0; k < holesLength; k++)
{
if (overlappingOutlines[i] == false || overlappingHoles[k] == false)
{
if (OverlappingBoundingBoxes(outlines[i], holes[k]))
{
overlappingOutlines[i] = true;
overlappingHoles[k] = true;
anyOutlinesOverlapping = true;
anyHolesOverlapping = true;
}
}
}
}
this.outlines = newOutlines;
this.holes = newHoles;
if (anyOutlinesOverlapping)
{
var clipper = new ClipperLib.Clipper();
// extract the final list of paths out of the new polygons we just converted down to.
this.paths = newOutlines.Union(newHoles).ToArray();
// 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);
List<IShape> newOutlines = new List<IShape>();
List<IShape> newHoles = new List<IShape>();
// convert the 'tree' back to shapes
this.ExtractOutlines(tree, newOutlines, newHoles);
// add the origional outlines that where not overlapping
for (int i = 0; i < outlineLength - 1; i++)
{
if (!overlappingOutlines[i])
{
newOutlines.Add(outlines[i]);
}
}
this.outlines = newOutlines.ToArray();
if (newHoles.Count > 0)
{
this.holes = newHoles.ToArray();
}
}else
{
this.outlines = outlines;
}
var paths = new List<IPath>();
foreach (var o in this.outlines)
{
paths.AddRange(o);
}
if (this.holes != null)
{
foreach (var o in this.holes)
{
paths.AddRange(o);
}
}
this.paths = paths;
}
}
}

10
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>

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