From 691c214a1168b6d22146bbf438b7c2993042771b Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 16 Dec 2016 16:57:21 +0000 Subject: [PATCH 1/3] 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. --- src/ImageSharp/Drawing/Paths/InternalPath.cs | 25 ++- .../Drawing/Shapes/ComplexPolygon.cs | 146 +++++++++++++++--- src/ImageSharp/Drawing/Shapes/Polygon.cs | 10 ++ .../Drawing/LineComplexPolygonTests.cs | 49 ++++++ 4 files changed, 202 insertions(+), 28 deletions(-) diff --git a/src/ImageSharp/Drawing/Paths/InternalPath.cs b/src/ImageSharp/Drawing/Paths/InternalPath.cs index b34702253..b71ba62a8 100644 --- a/src/ImageSharp/Drawing/Paths/InternalPath.cs +++ b/src/ImageSharp/Drawing/Paths/InternalPath.cs @@ -60,10 +60,29 @@ namespace ImageSharp.Drawing.Paths /// The segments. /// if set to true [is closed path]. internal InternalPath(ILineSegment[] segments, bool isClosedPath) + : this(Simplify(segments), isClosedPath) { - Guard.NotNull(segments, nameof(segments)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The segment. + /// if set to true [is closed path]. + internal InternalPath(ILineSegment segment, bool isClosedPath) + :this(segment.AsSimpleLinearPath(), isClosedPath) + { + } + - this.points = this.Simplify(segments); + /// + /// Initializes a new instance of the class. + /// + /// The points. + /// if set to true [is closed path]. + 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 /// /// The . /// - private Vector2[] Simplify(ILineSegment[] segments) + private static Vector2[] Simplify(ILineSegment[] segments) { List simplified = new List(); foreach (ILineSegment seg in segments) diff --git a/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs b/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs index d0edb76aa..7bd459f6a 100644 --- a/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs +++ b/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 holes; - private IEnumerable outlines; + private IShape[] holes; + private IShape[] outlines; private IEnumerable paths; /// @@ -39,7 +38,7 @@ namespace ImageSharp.Drawing.Shapes /// /// The outlines. /// The holes. - public ComplexPolygon(IEnumerable outlines, IEnumerable 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 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 outlines, List holes) + + private void ExtractOutlines(ClipperLib.PolyNode tree, List outlines, List 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 outlines, IEnumerable holes) + /// + /// Determines if the s bounding boxes overlap. + /// + /// The source. + /// The target. + /// + 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 newOutlines = new List(); - List newHoles = new List(); + 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 newOutlines = new List(); + List newHoles = new List(); + + // 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(); + 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; } } } \ No newline at end of file diff --git a/src/ImageSharp/Drawing/Shapes/Polygon.cs b/src/ImageSharp/Drawing/Shapes/Polygon.cs index 9d9626d4e..3621a9e2c 100644 --- a/src/ImageSharp/Drawing/Shapes/Polygon.cs +++ b/src/ImageSharp/Drawing/Shapes/Polygon.cs @@ -29,6 +29,16 @@ namespace ImageSharp.Drawing.Shapes this.pathCollection = new[] { this }; } + /// + /// Initializes a new instance of the class. + /// + /// The segment. + public Polygon(ILineSegment segment) + { + this.innerPath = new InternalPath(segment, true); + this.pathCollection = new[] { this }; + } + /// /// Gets the bounding box of this shape. /// diff --git a/tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs b/tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs index 1f6708bf8..cdcb5ebe4 100644 --- a/tests/ImageSharp.Tests/Drawing/LineComplexPolygonTests.cs +++ b/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() From 0cf795fc27a8a3ecf241fda6b5d0478d85692168 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 16 Dec 2016 17:25:46 +0000 Subject: [PATCH 2/3] Remove more linq calls removed a Linq call from the critical path of geting distance from point. --- .../Drawing/Shapes/ComplexPolygon.cs | 92 ++++++++----------- 1 file changed, 40 insertions(+), 52 deletions(-) diff --git a/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs b/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs index 7bd459f6a..32f5678a7 100644 --- a/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs +++ b/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs @@ -7,6 +7,7 @@ namespace ImageSharp.Drawing.Shapes { using System.Collections; using System.Collections.Generic; + using System.Linq; using System.Numerics; using Paths; @@ -19,8 +20,7 @@ namespace ImageSharp.Drawing.Shapes public sealed class ComplexPolygon : IShape { private const float ClipperScaleFactor = 100f; - private IShape[] holes; - private IShape[] outlines; + private IShape[] shapes; private IEnumerable paths; /// @@ -41,14 +41,14 @@ namespace ImageSharp.Drawing.Shapes 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); } @@ -68,30 +68,36 @@ namespace ImageSharp.Drawing.Shapes /// /// Returns the distance from thr shape to the point /// + /// + /// 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 :) + /// 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 && this.holes != null) + 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; } @@ -149,7 +155,7 @@ namespace ImageSharp.Drawing.Shapes } - private void ExtractOutlines(ClipperLib.PolyNode tree, List outlines, List holes) + private void ExtractOutlines(ClipperLib.PolyNode tree, List shapes) { if (tree.Contour.Any()) { @@ -164,19 +170,12 @@ namespace ImageSharp.Drawing.Shapes var polygon = new Polygon(new LinearLineSegment(vectors)); - if (tree.IsHole) - { - holes.Add(polygon); - } - else - { - outlines.Add(polygon); - } + shapes.Add(polygon); } foreach (var c in tree.Childs) { - this.ExtractOutlines(c, outlines, holes); + this.ExtractOutlines(c, shapes); } } @@ -198,7 +197,7 @@ namespace ImageSharp.Drawing.Shapes // 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; + int holesLength = holes?.Length ?? 0; bool[] overlappingOutlines = new bool[outlineLength]; bool[] overlappingHoles = new bool[holesLength]; bool anyOutlinesOverlapping = false; @@ -249,43 +248,32 @@ namespace ImageSharp.Drawing.Shapes var tree = new ClipperLib.PolyTree(); clipper.Execute(ClipperLib.ClipType.ctDifference, tree); - List newOutlines = new List(); - List newHoles = new List(); + List newShapes = new List(); // convert the 'tree' back to shapes - this.ExtractOutlines(tree, newOutlines, newHoles); + this.ExtractOutlines(tree, newShapes); // add the origional outlines that where not overlapping for (int i = 0; i < outlineLength - 1; i++) { if (!overlappingOutlines[i]) { - newOutlines.Add(outlines[i]); + newShapes.Add(outlines[i]); } } - this.outlines = newOutlines.ToArray(); - if (newHoles.Count > 0) - { - this.holes = newHoles.ToArray(); - } + this.shapes = newShapes.ToArray(); }else { - this.outlines = outlines; + this.shapes = outlines; } var paths = new List(); - foreach (var o in this.outlines) + foreach (var o in this.shapes) { paths.AddRange(o); } - if (this.holes != null) - { - foreach (var o in this.holes) - { - paths.AddRange(o); - } - } + this.paths = paths; } } From 18d87956dc8b18315541cf1e6fe4fdb16fd52238 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 16 Dec 2016 18:58:46 +0000 Subject: [PATCH 3/3] fix stylecop issues --- src/ImageSharp/Drawing/Paths/InternalPath.cs | 3 +- .../Drawing/Shapes/ComplexPolygon.cs | 33 +++++++++---------- src/ImageSharp/Drawing/Shapes/Polygon.cs | 3 +- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/ImageSharp/Drawing/Paths/InternalPath.cs b/src/ImageSharp/Drawing/Paths/InternalPath.cs index b71ba62a8..52d43b6e8 100644 --- a/src/ImageSharp/Drawing/Paths/InternalPath.cs +++ b/src/ImageSharp/Drawing/Paths/InternalPath.cs @@ -70,11 +70,10 @@ namespace ImageSharp.Drawing.Paths /// The segment. /// if set to true [is closed path]. internal InternalPath(ILineSegment segment, bool isClosedPath) - :this(segment.AsSimpleLinearPath(), isClosedPath) + : this(segment.AsSimpleLinearPath(), isClosedPath) { } - /// /// Initializes a new instance of the class. /// diff --git a/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs b/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs index 32f5678a7..d42cae872 100644 --- a/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs +++ b/src/ImageSharp/Drawing/Shapes/ComplexPolygon.cs @@ -68,9 +68,9 @@ namespace ImageSharp.Drawing.Shapes /// /// Returns the distance from thr shape to the point /// - /// - /// 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 + /// + /// 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 :) /// float IShape.Distance(Vector2 point) @@ -81,20 +81,21 @@ namespace ImageSharp.Drawing.Shapes { var d = shape.Distance(point); - if(d <= 0) + if (d <= 0) { // we are inside a poly d = -d; // flip the sign - inside ^= true; // flip the inside flag + inside ^= true; // flip the inside flag } - if(d < dist) + if (d < dist) { dist = d; } } - if (inside) { + if (inside) + { return -dist; } @@ -145,7 +146,7 @@ namespace ImageSharp.Drawing.Shapes private void AddPoints(ClipperLib.Clipper clipper, IShape[] shapes, bool[] shouldInclude, ClipperLib.PolyType polyType) { - for(var i =0; i< shapes.Length; i++) + for (var i = 0; i < shapes.Length; i++) { if (shouldInclude[i]) { @@ -154,7 +155,6 @@ namespace ImageSharp.Drawing.Shapes } } - private void ExtractOutlines(ClipperLib.PolyNode tree, List shapes) { if (tree.Contour.Any()) @@ -162,7 +162,7 @@ namespace ImageSharp.Drawing.Shapes // convert the Clipper Contour from scaled ints back down to the origional size (this is going to be lossy but not significantly) var pointCount = tree.Contour.Count; var vectors = new Vector2[pointCount]; - for(var i=0; i< pointCount; i++) + for (var i = 0; i < pointCount; i++) { var p = tree.Contour[i]; vectors[i] = new Vector2(p.X, p.Y) / ClipperScaleFactor; @@ -184,18 +184,16 @@ namespace ImageSharp.Drawing.Shapes /// /// The source. /// The target. - /// + /// true if the 2 shapes bounding boxes overlap. private bool OverlappingBoundingBoxes(IShape source, IShape target) { return source.Bounds.Intersects(target.Bounds); } - 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 - int outlineLength = outlines.Length; int holesLength = holes?.Length ?? 0; bool[] overlappingOutlines = new bool[outlineLength]; @@ -207,10 +205,10 @@ namespace ImageSharp.Drawing.Shapes { for (int j = i + 1; j < outlineLength; j++) { - //skip the bounds check if they are already tested + // skip the bounds check if they are already tested if (overlappingOutlines[i] == false || overlappingOutlines[j] == false) { - if (OverlappingBoundingBoxes(outlines[i], outlines[j])) + if (this.OverlappingBoundingBoxes(outlines[i], outlines[j])) { overlappingOutlines[i] = true; overlappingOutlines[j] = true; @@ -223,7 +221,7 @@ namespace ImageSharp.Drawing.Shapes { if (overlappingOutlines[i] == false || overlappingHoles[k] == false) { - if (OverlappingBoundingBoxes(outlines[i], holes[k])) + if (this.OverlappingBoundingBoxes(outlines[i], holes[k])) { overlappingOutlines[i] = true; overlappingHoles[k] = true; @@ -263,7 +261,8 @@ namespace ImageSharp.Drawing.Shapes } this.shapes = newShapes.ToArray(); - }else + } + else { this.shapes = outlines; } diff --git a/src/ImageSharp/Drawing/Shapes/Polygon.cs b/src/ImageSharp/Drawing/Shapes/Polygon.cs index 3621a9e2c..6da27cf48 100644 --- a/src/ImageSharp/Drawing/Shapes/Polygon.cs +++ b/src/ImageSharp/Drawing/Shapes/Polygon.cs @@ -108,8 +108,7 @@ namespace ImageSharp.Drawing.Shapes /// /// Calcualtes the distance along and away from the path for a specified point. /// - /// The x. - /// The y. + /// The point along the path. /// /// distance metadata about the point. ///