From bea633161bec167dad6a282008d8ef1b72112410 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Wed, 4 Jan 2017 08:36:42 +0000 Subject: [PATCH] WIP - Improve polygon fill speed --- src/ImageSharp.Drawing/Paths/InternalPath.cs | 130 +++++++++ .../Processors/FillShapeProcessorFast.cs | 268 ++++++++++++++++++ .../Shapes/BezierPolygon.cs | 24 ++ .../Shapes/ComplexPolygon.cs | 37 ++- src/ImageSharp.Drawing/Shapes/IShape.cs | 21 ++ .../Shapes/LinearPolygon.cs | 30 ++ src/ImageSharp.Drawing/Shapes/Polygon.cs | 24 ++ .../Shapes/RectangularPolygon.cs | 24 ++ .../Drawing/FillPolygonStatagies.cs | 49 ++++ 9 files changed, 605 insertions(+), 2 deletions(-) create mode 100644 src/ImageSharp.Drawing/Processors/FillShapeProcessorFast.cs create mode 100644 tests/ImageSharp.Benchmarks/Drawing/FillPolygonStatagies.cs diff --git a/src/ImageSharp.Drawing/Paths/InternalPath.cs b/src/ImageSharp.Drawing/Paths/InternalPath.cs index 52d43b6e8c..aab71937de 100644 --- a/src/ImageSharp.Drawing/Paths/InternalPath.cs +++ b/src/ImageSharp.Drawing/Paths/InternalPath.cs @@ -14,6 +14,11 @@ namespace ImageSharp.Drawing.Paths /// internal class InternalPath { + /// + /// The maximum vector + /// + private static readonly Vector2 MaxVector = new Vector2(float.MaxValue); + /// /// The locker. /// @@ -163,6 +168,45 @@ namespace ImageSharp.Drawing.Paths }; } + /// + /// Finds the intersections. + /// + /// The start. + /// The end. + /// The buffer. + /// The count. + /// The offset. + /// number iof intersections hit + public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset) + { + int polyCorners = this.points.Length; + + if (!this.closedPath) + { + polyCorners -= 1; + } + + int position = 0; + for (int i = 0; i < polyCorners && count > 0; i++) + { + int next = i + 1; + if (this.closedPath && next == polyCorners) + { + next = 0; + } + + var point = FindIntersection(this.points[i], this.points[next], start, end); + if (point != MaxVector) + { + buffer[position + offset] = point; + position++; + count--; + } + } + + return position; + } + /// /// Points the in polygon. /// @@ -203,6 +247,92 @@ namespace ImageSharp.Drawing.Paths return oddNodes; } + private static bool BoundingBoxesIntersect(Vector2 line1Start, Vector2 line1End, Vector2 line2Start, Vector2 line2End) + { + var topLeft1 = Vector2.Min(line1Start, line1End); + var bottomRight1 = Vector2.Max(line1Start, line1End); + + var topLeft2 = Vector2.Min(line2Start, line2End); + var bottomRight2 = Vector2.Max(line2Start, line2End); + + var left1 = topLeft1.X; + var right1 = bottomRight1.X; + var top1 = topLeft1.Y; + var bottom1 = bottomRight1.Y; + + var left2 = topLeft2.X; + var right2 = bottomRight2.X; + var top2 = topLeft2.Y; + var bottom2 = bottomRight2.Y; + + return left1 <= right2 && right1 >= left2 + && + top1 <= bottom2 && bottom1 >= top2; + } + + private static Vector2 FindIntersection(Vector2 line1Start, Vector2 line1End, Vector2 line2Start, Vector2 line2End) + { + // do lines cross at all + if (!BoundingBoxesIntersect(line1Start, line1End, line2Start, line2End)) + { + return MaxVector; + } + + var line1Diff = line1End - line1Start; + var line2Diff = line2End - line2Start; + + Vector2 point; + if (line1Diff.X == 0) + { + float slope = line2Diff.Y / line2Diff.X; + + var yinter = line2Start.Y - (slope * line2Start.X); + var y = (line1Start.X * slope) + yinter; + point = new Vector2(line1Start.X, y); + + // horizontal and vertical lines + } + else if (line2Diff.X == 0) + { + float slope = line1Diff.Y / line1Diff.X; + var yinter = line1Start.Y - (slope * line1Start.X); + var y = (line2Start.X * slope) + yinter; + point = new Vector2(line2Start.X, y); + + // horizontal and vertical lines + } + else + { + float slope1 = line1Diff.Y / line1Diff.X; + float slope2 = line2Diff.Y / line2Diff.X; + + var yinter1 = line1Start.Y - (slope1 * line1Start.X); + var yinter2 = line2Start.Y - (slope2 * line2Start.X); + + if (slope1 == slope2 && yinter1 != yinter2) + { + return MaxVector; + } + + float x = (yinter2 - yinter1) / (slope1 - slope2); + + float y = (slope1 * x) + yinter1; + + point = new Vector2(x, y); + } + + if (BoundingBoxesIntersect(line1Start, line1End, point, point)) + { + return point; + } + else if (BoundingBoxesIntersect(line2Start, line2End, point, point)) + { + return point; + } + + return MaxVector; + } + /// /// Simplifies the collection of segments. /// diff --git a/src/ImageSharp.Drawing/Processors/FillShapeProcessorFast.cs b/src/ImageSharp.Drawing/Processors/FillShapeProcessorFast.cs new file mode 100644 index 0000000000..29b2b01d1b --- /dev/null +++ b/src/ImageSharp.Drawing/Processors/FillShapeProcessorFast.cs @@ -0,0 +1,268 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Drawing.Processors +{ + using System; + using System.Buffers; + using System.Numerics; + using System.Threading.Tasks; + using Drawing; + using ImageSharp.Processing; + using Shapes; + using Rectangle = ImageSharp.Rectangle; + + /// + /// Usinf a brsuh and a shape fills shape with contents of brush the + /// + /// The type of the color. + /// + public class FillShapeProcessorFast : ImageProcessor + where TColor : struct, IPackedPixel, IEquatable + { + private const float AntialiasFactor = 1f; + private const int DrawPadding = 1; + private readonly IBrush fillColor; + private readonly IShape poly; + private readonly GraphicsOptions options; + + /// + /// Initializes a new instance of the class. + /// + /// The brush. + /// The shape. + /// The options. + public FillShapeProcessorFast(IBrush brush, IShape shape, GraphicsOptions options) + { + this.poly = shape; + this.fillColor = brush; + this.options = options; + } + + /// + protected override void OnApply(ImageBase source, Rectangle sourceRectangle) + { + Rectangle rect = RectangleF.Ceiling(this.poly.Bounds); // rounds the points out away from the center + + int polyStartY = rect.Y - DrawPadding; + int polyEndY = rect.Bottom + DrawPadding; + int startX = rect.X - DrawPadding; + int endX = rect.Right + DrawPadding; + + int minX = Math.Max(sourceRectangle.Left, startX); + int maxX = Math.Min(sourceRectangle.Right - 1, endX); + int minY = Math.Max(sourceRectangle.Top, polyStartY); + int maxY = Math.Min(sourceRectangle.Bottom - 1, polyEndY); + + // Align start/end positions. + minX = Math.Max(0, minX); + maxX = Math.Min(source.Width, maxX); + minY = Math.Max(0, minY); + maxY = Math.Min(source.Height, maxY); + + // Reset offset if necessary. + if (minX > 0) + { + startX = 0; + } + + if (minY > 0) + { + polyStartY = 0; + } + + ArrayPool arrayPool = ArrayPool.Shared; + + int maxIntersections = this.poly.MaxIntersections; + + using (PixelAccessor sourcePixels = source.Lock()) + using (IBrushApplicator applicator = this.fillColor.CreateApplicator(sourcePixels, rect)) + { + // we need to repeat this vertically to set anitialiasing vertically + // but we only have to get colors/fills for the external points nearest transitions in the X Pass ands only is anitialiasing is enabled + Parallel.For( + minY, + maxY, + this.ParallelOptions, + y => + { + int offsetY = y - polyStartY; + var buffer = arrayPool.Rent(maxIntersections); + var left = new Vector2(startX, offsetY); + var right = new Vector2(endX, offsetY); + + // foreach line we get all the points where this line crosses the polygon + var pointsFound = this.poly.FindIntersections(left, right, buffer, maxIntersections, 0); + if (pointsFound == 0) + { + arrayPool.Return(buffer); + + // nothign on this line skip + return; + } + + QuickSort(buffer, 0, pointsFound); + + int currentIntersection = 0; + float nextPoint = buffer[0].X; + float lastPoint = left.X; + float targetPoint = nextPoint; + bool isInside = false; + + // every odd point is the start of a line + Vector2 currentPoint = default(Vector2); + + for (int x = minX; x < maxX; x++) + { + int offsetX = x - startX; + currentPoint.X = offsetX; + currentPoint.Y = offsetY; + if (!isInside) + { + if (offsetX < (nextPoint - DrawPadding) && offsetX > (lastPoint + DrawPadding)) + { + if (nextPoint == right.X) + { + // we are in the ends run skip it + x = maxX; + continue; + } + + // lets just jump forward + x = (int)Math.Floor(nextPoint) + startX - DrawPadding; + } + } + bool onCorner = false; + + // there seems to be some issue with this switch. + if (offsetX >= nextPoint) + { + currentIntersection++; + lastPoint = nextPoint; + if (currentIntersection == pointsFound) + { + nextPoint = right.X; + } + else + { + nextPoint = buffer[currentIntersection].X; + + // double point from a corner flip the bit back and move on again + if (nextPoint == lastPoint) + { + onCorner = true; + isInside ^= true; + currentIntersection++; + if (currentIntersection == pointsFound) + { + nextPoint = right.X; + } + else + { + nextPoint = buffer[currentIntersection].X; + } + } + } + + isInside ^= true; + } + + float opacity = 1; + if (!isInside && !onCorner) + { + if (this.options.Antialias) + { + float distance = float.MaxValue; + if (offsetX == lastPoint || offsetX == nextPoint) + { + // we are to far away from the line + distance = 0; + } + else if (nextPoint - AntialiasFactor < offsetX) + { + // we are near the left of the line + distance = nextPoint - offsetX; + } + else if (lastPoint + AntialiasFactor > offsetX) + { + // we are near the right of the line + distance = offsetX - lastPoint; + } + else + { + // we are to far away from the line + continue; + } + opacity = 1 - (distance / AntialiasFactor); + } + else + { + continue; + } + } + + if (opacity > Constants.Epsilon) + { + Vector4 backgroundVector = sourcePixels[offsetX, offsetY].ToVector4(); + Vector4 sourceVector = applicator.GetColor(currentPoint).ToVector4(); + + Vector4 finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, opacity); + finalColor.W = backgroundVector.W; + + TColor packed = default(TColor); + packed.PackFromVector4(finalColor); + sourcePixels[offsetX, offsetY] = packed; + } + } + + arrayPool.Return(buffer); + }); + } + } + + private static void QuickSort(Vector2[] data, int left, int right) + { + int i = left - 1; + int j = right; + + while (true) + { + float d = data[left].X; + do + { + i++; + } + while (data[i].X < d); + + do + { + j--; + } + while (data[j].X > d); + + if (i < j) + { + Vector2 tmp = data[i]; + data[i] = data[j]; + data[j] = tmp; + } + else + { + if (left < j) + { + QuickSort(data, left, j); + } + + if (++j < right) + { + QuickSort(data, j, right); + } + + return; + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Shapes/BezierPolygon.cs b/src/ImageSharp.Drawing/Shapes/BezierPolygon.cs index 0365588238..1f32814cb6 100644 --- a/src/ImageSharp.Drawing/Shapes/BezierPolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/BezierPolygon.cs @@ -34,6 +34,14 @@ namespace ImageSharp.Drawing.Shapes /// public RectangleF Bounds => this.innerPolygon.Bounds; + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections => this.innerPolygon.MaxIntersections; + /// /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds /// @@ -43,6 +51,22 @@ namespace ImageSharp.Drawing.Shapes /// public float Distance(Vector2 point) => this.innerPolygon.Distance(point); + /// + /// Finds the intersections. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset) + { + return this.innerPolygon.FindIntersections(start, end, buffer, count, offset); + } + /// /// Returns an enumerator that iterates through the collection. /// diff --git a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs b/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs index d9b83d3415..32de68fb9f 100644 --- a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs @@ -5,6 +5,7 @@ namespace ImageSharp.Drawing.Shapes { + using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -44,7 +45,7 @@ namespace ImageSharp.Drawing.Shapes Guard.NotNull(outlines, nameof(outlines)); Guard.MustBeGreaterThanOrEqualTo(outlines.Length, 1, nameof(outlines)); - this.FixAndSetShapes(outlines, holes); + this.MaxIntersections = this.FixAndSetShapes(outlines, holes); var minX = this.shapes.Min(x => x.Bounds.Left); var maxX = this.shapes.Max(x => x.Bounds.Right); @@ -62,6 +63,14 @@ namespace ImageSharp.Drawing.Shapes /// public RectangleF Bounds { get; } + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections { get; } + /// /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds /// @@ -103,6 +112,22 @@ namespace ImageSharp.Drawing.Shapes return dist; } + /// + /// Finds the intersections. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset) + { + throw new NotImplementedException(); + } + /// /// Returns an enumerator that iterates through the collection. /// @@ -180,7 +205,7 @@ namespace ImageSharp.Drawing.Shapes } } - private void FixAndSetShapes(IEnumerable outlines, IEnumerable holes) + private int FixAndSetShapes(IEnumerable outlines, IEnumerable holes) { var clipper = new Clipper(); @@ -197,6 +222,14 @@ namespace ImageSharp.Drawing.Shapes this.ExtractOutlines(tree, shapes, paths); this.shapes = shapes.ToArray(); this.paths = paths.ToArray(); + + int intersections = 0; + foreach (var s in this.shapes) + { + intersections += s.MaxIntersections; + } + + return intersections; } } } \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Shapes/IShape.cs b/src/ImageSharp.Drawing/Shapes/IShape.cs index 2640b33aa4..5c9afc1f99 100644 --- a/src/ImageSharp.Drawing/Shapes/IShape.cs +++ b/src/ImageSharp.Drawing/Shapes/IShape.cs @@ -22,6 +22,14 @@ namespace ImageSharp.Drawing.Shapes /// RectangleF Bounds { get; } + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + int MaxIntersections { get; } + /// /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds /// @@ -30,5 +38,18 @@ namespace ImageSharp.Drawing.Shapes /// Returns the distance from the shape to the point /// float Distance(Vector2 point); + + /// + /// Finds the intersections. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset); } } diff --git a/src/ImageSharp.Drawing/Shapes/LinearPolygon.cs b/src/ImageSharp.Drawing/Shapes/LinearPolygon.cs index 03069b6785..d590cbabff 100644 --- a/src/ImageSharp.Drawing/Shapes/LinearPolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/LinearPolygon.cs @@ -34,6 +34,20 @@ namespace ImageSharp.Drawing.Shapes /// public RectangleF Bounds => this.innerPolygon.Bounds; + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections + { + get + { + return this.innerPolygon.MaxIntersections; + } + } + /// /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds /// @@ -43,6 +57,22 @@ namespace ImageSharp.Drawing.Shapes /// public float Distance(Vector2 point) => this.innerPolygon.Distance(point); + /// + /// Finds the intersections. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset) + { + return this.innerPolygon.FindIntersections(start, end, buffer, count, offset); + } + /// /// Returns an enumerator that iterates through the collection. /// diff --git a/src/ImageSharp.Drawing/Shapes/Polygon.cs b/src/ImageSharp.Drawing/Shapes/Polygon.cs index 6da27cf488..9fcb4a6a10 100644 --- a/src/ImageSharp.Drawing/Shapes/Polygon.cs +++ b/src/ImageSharp.Drawing/Shapes/Polygon.cs @@ -63,6 +63,14 @@ namespace ImageSharp.Drawing.Shapes /// public bool IsClosed => true; + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections => this.innerPath.Points.Length; + /// /// the distance of the point from the outline of the shape, if the value is negative it is inside the polygon bounds /// @@ -127,5 +135,21 @@ namespace ImageSharp.Drawing.Shapes { return this.innerPath.Points; } + + /// + /// Finds the intersections. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset) + { + return this.innerPath.FindIntersections(start, end, buffer, count, offset); + } } } diff --git a/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs b/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs index f05dadc7cc..170653c143 100644 --- a/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs @@ -79,6 +79,14 @@ namespace ImageSharp.Drawing.Shapes /// public float Length { get; } + /// + /// Gets the maximum number intersections that a shape can have when testing a line. + /// + /// + /// The maximum intersections. + /// + public int MaxIntersections => 4; + /// /// Calculates the distance along and away from the path for a specified point. /// @@ -141,6 +149,22 @@ namespace ImageSharp.Drawing.Shapes return this.points; } + /// + /// Finds the intersections. + /// + /// The start point of the line. + /// The end point of the line. + /// The buffer that will be populated with intersections. + /// The count. + /// The offset. + /// + /// The number of intersections populated into the buffer. + /// + public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset) + { + throw new NotImplementedException(); + } + private PointInfo Distance(Vector2 point, bool getDistanceAwayOnly, out bool isInside) { // point in rectangle diff --git a/tests/ImageSharp.Benchmarks/Drawing/FillPolygonStatagies.cs b/tests/ImageSharp.Benchmarks/Drawing/FillPolygonStatagies.cs new file mode 100644 index 0000000000..5018ed8f12 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Drawing/FillPolygonStatagies.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Benchmarks +{ + using System.Drawing; + using System.Drawing.Drawing2D; + using System.IO; + using System.Numerics; + + using BenchmarkDotNet.Attributes; + + using CoreColor = ImageSharp.Color; + using CoreImage = ImageSharp.Image; + using Drawing.Processors; + + public class FillPolygonStatagies : BenchmarkBase + { + [Benchmark(Baseline = true, Description = "Simple Fill Polygon")] + public void DrawSolidPolygonSimple() + { + CoreImage image = new CoreImage(800, 800); + var brush = Drawing.Brushes.Brushes.Solid(CoreColor.HotPink); + var shape = new Drawing.Shapes.LinearPolygon(new[] { + new Vector2(10, 10), + new Vector2(550, 50), + new Vector2(200, 400) + }); + + image.Apply(new FillShapeProcessor(brush, shape, Drawing.GraphicsOptions.Default)); + } + + [Benchmark(Description = "Fast Fill Polygon")] + public void DrawSolidPolygonFast() + { + CoreImage image = new CoreImage(800, 800); + var brush = Drawing.Brushes.Brushes.Solid(CoreColor.HotPink); + var shape = new Drawing.Shapes.LinearPolygon(new[] { + new Vector2(10, 10), + new Vector2(550, 50), + new Vector2(200, 400) + }); + + image.Apply(new FillShapeProcessorFast(brush, shape, Drawing.GraphicsOptions.Default)); + } + } +} \ No newline at end of file