From fd301f288413d8f3585143319e0f8f54b1cb13ce Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Wed, 4 Jan 2017 13:59:31 +0000 Subject: [PATCH] skip blank sections while filling polygons --- src/ImageSharp.Drawing/Fill.cs | 4 +- src/ImageSharp.Drawing/FillRectangle.cs | 4 +- .../Processors/FillShapeProcessorFast.cs | 258 +++++++++++++++--- .../Shapes/RectangularPolygon.cs | 20 +- src/ImageSharp/ImageProcessor.cs | 4 + .../Drawing/FillPolygonStatagies.cs | 36 ++- .../Drawing/SolidBezierTests.cs | 1 + .../Drawing/SolidPolygonTests.cs | 2 +- 8 files changed, 282 insertions(+), 47 deletions(-) diff --git a/src/ImageSharp.Drawing/Fill.cs b/src/ImageSharp.Drawing/Fill.cs index c0f43bdd18..1e14d8b11c 100644 --- a/src/ImageSharp.Drawing/Fill.cs +++ b/src/ImageSharp.Drawing/Fill.cs @@ -56,7 +56,7 @@ namespace ImageSharp public static Image Fill(this Image source, IBrush brush, IShape shape, GraphicsOptions options) where TColor : struct, IPackedPixel, IEquatable { - return source.Apply(new FillShapeProcessor(brush, shape, options)); + return source.Apply(new FillShapeProcessorFast(brush, shape, options)); } /// @@ -70,7 +70,7 @@ namespace ImageSharp public static Image Fill(this Image source, IBrush brush, IShape shape) where TColor : struct, IPackedPixel, IEquatable { - return source.Apply(new FillShapeProcessor(brush, shape, GraphicsOptions.Default)); + return source.Apply(new FillShapeProcessorFast(brush, shape, GraphicsOptions.Default)); } /// diff --git a/src/ImageSharp.Drawing/FillRectangle.cs b/src/ImageSharp.Drawing/FillRectangle.cs index d29b58e1b8..322133a4d5 100644 --- a/src/ImageSharp.Drawing/FillRectangle.cs +++ b/src/ImageSharp.Drawing/FillRectangle.cs @@ -31,7 +31,7 @@ namespace ImageSharp public static Image Fill(this Image source, IBrush brush, RectangleF shape, GraphicsOptions options) where TColor : struct, IPackedPixel, IEquatable { - return source.Apply(new FillShapeProcessor(brush, new RectangularPolygon(shape), options)); + return source.Apply(new FillShapeProcessorFast(brush, new RectangularPolygon(shape), options)); } /// @@ -45,7 +45,7 @@ namespace ImageSharp public static Image Fill(this Image source, IBrush brush, RectangleF shape) where TColor : struct, IPackedPixel, IEquatable { - return source.Apply(new FillShapeProcessor(brush, new RectangularPolygon(shape), GraphicsOptions.Default)); + return source.Apply(new FillShapeProcessorFast(brush, new RectangularPolygon(shape), GraphicsOptions.Default)); } /// diff --git a/src/ImageSharp.Drawing/Processors/FillShapeProcessorFast.cs b/src/ImageSharp.Drawing/Processors/FillShapeProcessorFast.cs index 29b2b01d1b..d192dc6c3a 100644 --- a/src/ImageSharp.Drawing/Processors/FillShapeProcessorFast.cs +++ b/src/ImageSharp.Drawing/Processors/FillShapeProcessorFast.cs @@ -62,17 +62,6 @@ namespace ImageSharp.Drawing.Processors 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; @@ -80,18 +69,15 @@ namespace ImageSharp.Drawing.Processors 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); + var left = new Vector2(startX, y); + var right = new Vector2(endX, y); // foreach line we get all the points where this line crosses the polygon var pointsFound = this.poly.FindIntersections(left, right, buffer, maxIntersections, 0); @@ -103,11 +89,11 @@ namespace ImageSharp.Drawing.Processors return; } - QuickSort(buffer, 0, pointsFound); + QuickSortX(buffer, 0, pointsFound); int currentIntersection = 0; float nextPoint = buffer[0].X; - float lastPoint = left.X; + float lastPoint = float.MinValue; float targetPoint = nextPoint; bool isInside = false; @@ -116,12 +102,11 @@ namespace ImageSharp.Drawing.Processors for (int x = minX; x < maxX; x++) { - int offsetX = x - startX; - currentPoint.X = offsetX; - currentPoint.Y = offsetY; + currentPoint.X = x; + currentPoint.Y = y; if (!isInside) { - if (offsetX < (nextPoint - DrawPadding) && offsetX > (lastPoint + DrawPadding)) + if (x < (nextPoint - DrawPadding) && x > (lastPoint + DrawPadding)) { if (nextPoint == right.X) { @@ -131,13 +116,13 @@ namespace ImageSharp.Drawing.Processors } // lets just jump forward - x = (int)Math.Floor(nextPoint) + startX - DrawPadding; + x = (int)Math.Floor(nextPoint) - DrawPadding; } } bool onCorner = false; // there seems to be some issue with this switch. - if (offsetX >= nextPoint) + if (x >= nextPoint) { currentIntersection++; lastPoint = nextPoint; @@ -175,20 +160,20 @@ namespace ImageSharp.Drawing.Processors if (this.options.Antialias) { float distance = float.MaxValue; - if (offsetX == lastPoint || offsetX == nextPoint) + if (x == lastPoint || x == nextPoint) { // we are to far away from the line distance = 0; } - else if (nextPoint - AntialiasFactor < offsetX) + else if (nextPoint - AntialiasFactor < x) { // we are near the left of the line - distance = nextPoint - offsetX; + distance = nextPoint - x; } - else if (lastPoint + AntialiasFactor > offsetX) + else if (lastPoint + AntialiasFactor > x) { // we are near the right of the line - distance = offsetX - lastPoint; + distance = x - lastPoint; } else { @@ -205,7 +190,7 @@ namespace ImageSharp.Drawing.Processors if (opacity > Constants.Epsilon) { - Vector4 backgroundVector = sourcePixels[offsetX, offsetY].ToVector4(); + Vector4 backgroundVector = sourcePixels[x, y].ToVector4(); Vector4 sourceVector = applicator.GetColor(currentPoint).ToVector4(); Vector4 finalColor = Vector4BlendTransforms.PremultipliedLerp(backgroundVector, sourceVector, opacity); @@ -213,34 +198,233 @@ namespace ImageSharp.Drawing.Processors TColor packed = default(TColor); packed.PackFromVector4(finalColor); - sourcePixels[offsetX, offsetY] = packed; + sourcePixels[x, y] = packed; } } arrayPool.Return(buffer); }); + + if (this.options.Antialias) + { + // we only need to do the X can for antialiasing purposes + Parallel.For( + minX, + maxX, + this.ParallelOptions, + x => + { + var buffer = arrayPool.Rent(maxIntersections); + var left = new Vector2(x, polyStartY); + var right = new Vector2(x, polyEndY); + + // 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; + } + + QuickSortY(buffer, 0, pointsFound); + + int currentIntersection = 0; + float nextPoint = buffer[0].Y; + float lastPoint = left.Y; + float targetPoint = nextPoint; + bool isInside = false; + + // every odd point is the start of a line + Vector2 currentPoint = default(Vector2); + + for (int y = minY; y < maxY; y++) + { + currentPoint.X = x; + currentPoint.Y = y; + if (!isInside) + { + if (y < (nextPoint - DrawPadding) && y > (lastPoint + DrawPadding)) + { + if (nextPoint == right.Y) + { + // we are in the ends run skip it + y = maxY; + continue; + } + + // lets just jump forward + y = (int)Math.Floor(nextPoint) - DrawPadding; + } + } + else + { + if (y < nextPoint - DrawPadding) + { + if (nextPoint == right.Y) + { + // we are in the ends run skip it + y = maxY; + continue; + } + + // lets just jump forward + y = (int)Math.Floor(nextPoint); + } + } + + bool onCorner = false; + + if (y >= nextPoint) + { + currentIntersection++; + lastPoint = nextPoint; + if (currentIntersection == pointsFound) + { + nextPoint = right.Y; + } + else + { + nextPoint = buffer[currentIntersection].Y; + + // 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.Y; + } + else + { + nextPoint = buffer[currentIntersection].Y; + } + } + } + + isInside ^= true; + } + + float opacity = 1; + if (!isInside && !onCorner) + { + if (this.options.Antialias) + { + float distance = float.MaxValue; + if (y == lastPoint || y == nextPoint) + { + // we are to far away from the line + distance = 0; + } + else if (nextPoint - AntialiasFactor < y) + { + // we are near the left of the line + distance = nextPoint - y; + } + else if (lastPoint + AntialiasFactor > y) + { + // we are near the right of the line + distance = y - lastPoint; + } + else + { + // we are to far away from the line + continue; + } + opacity = 1 - (distance / AntialiasFactor); + } + else + { + continue; + } + } + + // don't set full opactiy color as it will have been gotten by the first scan + if (opacity > Constants.Epsilon && opacity < 1) + { + Vector4 backgroundVector = sourcePixels[x, y].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[x, y] = packed; + } + } + + arrayPool.Return(buffer); + }); + } + } + } + + private static void QuickSortX(Vector2[] data, int left, int right) + { + int i = left - 1; + int j = right; + + while (true) + { + float x = data[left].X; + do + { + i++; + } + while (data[i].X < x); + + do + { + j--; + } + while (data[j].X > x); + + if (i < j) + { + Vector2 tmp = data[i]; + data[i] = data[j]; + data[j] = tmp; + } + else + { + if (left < j) + { + QuickSortX(data, left, j); + } + + if (++j < right) + { + QuickSortX(data, j, right); + } + + return; + } } } - private static void QuickSort(Vector2[] data, int left, int right) + private static void QuickSortY(Vector2[] data, int left, int right) { int i = left - 1; int j = right; while (true) { - float d = data[left].X; + float d = data[left].Y; do { i++; } - while (data[i].X < d); + while (data[i].Y < d); do { j--; } - while (data[j].X > d); + while (data[j].Y > d); if (i < j) { @@ -252,12 +436,12 @@ namespace ImageSharp.Drawing.Processors { if (left < j) { - QuickSort(data, left, j); + QuickSortY(data, left, j); } if (++j < right) { - QuickSort(data, j, right); + QuickSortY(data, j, right); } return; diff --git a/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs b/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs index 170653c143..578616706f 100644 --- a/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs @@ -162,7 +162,25 @@ namespace ImageSharp.Drawing.Shapes /// public int FindIntersections(Vector2 start, Vector2 end, Vector2[] buffer, int count, int offset) { - throw new NotImplementedException(); + var discovered = 0; + Vector2 startPoint = Vector2.Clamp(start, this.topLeft, this.bottomRight); + Vector2 endPoint = Vector2.Clamp(end, this.topLeft, this.bottomRight); + + if (startPoint == Vector2.Clamp(startPoint, start, end)) + { + // if start closest is within line then its a valid point + discovered++; + buffer[offset++] = startPoint; + } + + if (endPoint == Vector2.Clamp(endPoint, start, end)) + { + // if start closest is within line then its a valid point + discovered++; + buffer[offset++] = endPoint; + } + + return discovered; } private PointInfo Distance(Vector2 point, bool getDistanceAwayOnly, out bool isInside) diff --git a/src/ImageSharp/ImageProcessor.cs b/src/ImageSharp/ImageProcessor.cs index 79bc3ee1e6..0697bf4ba5 100644 --- a/src/ImageSharp/ImageProcessor.cs +++ b/src/ImageSharp/ImageProcessor.cs @@ -39,7 +39,11 @@ namespace ImageSharp.Processing } catch (Exception ex) { +#if DEBUG + throw; +#else throw new ImageProcessingException($"An error occured when processing the image using {this.GetType().Name}. See the inner exception for more detail.", ex); +#endif } } diff --git a/tests/ImageSharp.Benchmarks/Drawing/FillPolygonStatagies.cs b/tests/ImageSharp.Benchmarks/Drawing/FillPolygonStatagies.cs index 5018ed8f12..a6f932439a 100644 --- a/tests/ImageSharp.Benchmarks/Drawing/FillPolygonStatagies.cs +++ b/tests/ImageSharp.Benchmarks/Drawing/FillPolygonStatagies.cs @@ -18,7 +18,7 @@ namespace ImageSharp.Benchmarks public class FillPolygonStatagies : BenchmarkBase { - [Benchmark(Baseline = true, Description = "Simple Fill Polygon")] + [Benchmark(Baseline = true, Description = "Simple Fill Polygon - antialias")] public void DrawSolidPolygonSimple() { CoreImage image = new CoreImage(800, 800); @@ -32,18 +32,46 @@ namespace ImageSharp.Benchmarks image.Apply(new FillShapeProcessor(brush, shape, Drawing.GraphicsOptions.Default)); } - [Benchmark(Description = "Fast Fill Polygon")] + [Benchmark(Description = "Fast Fill Polygon - antialias")] public void DrawSolidPolygonFast() { CoreImage image = new CoreImage(800, 800); var brush = Drawing.Brushes.Brushes.Solid(CoreColor.HotPink); - var shape = new Drawing.Shapes.LinearPolygon(new[] { + 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)); + } + + [Benchmark(Description = "Simple Fill Polygon - antialias disabled")] + public void DrawSolidPolygonSimpleDisableAntiAliase() + { + 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, new Drawing.GraphicsOptions(false))); + } + + [Benchmark(Description = "Fast Fill Polygon - antialias disabledp")] + public void DrawSolidPolygonFasteDisableAntiAliase() + { + 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)); + image.Apply(new FillShapeProcessorFast(brush, shape, new Drawing.GraphicsOptions(false))); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Drawing/SolidBezierTests.cs b/tests/ImageSharp.Tests/Drawing/SolidBezierTests.cs index f6bcf49065..43ed13b3c2 100644 --- a/tests/ImageSharp.Tests/Drawing/SolidBezierTests.cs +++ b/tests/ImageSharp.Tests/Drawing/SolidBezierTests.cs @@ -19,6 +19,7 @@ namespace ImageSharp.Tests.Drawing [Fact] public void ImageShouldBeOverlayedByFilledPolygon() { + Configuration.Default.ParallelOptions.MaxDegreeOfParallelism = 1; string path = CreateOutputDirectory("Drawing", "FilledBezier"); var simplePath = new[] { new Vector2(10, 400), diff --git a/tests/ImageSharp.Tests/Drawing/SolidPolygonTests.cs b/tests/ImageSharp.Tests/Drawing/SolidPolygonTests.cs index a70782478b..0d3b721d52 100644 --- a/tests/ImageSharp.Tests/Drawing/SolidPolygonTests.cs +++ b/tests/ImageSharp.Tests/Drawing/SolidPolygonTests.cs @@ -31,7 +31,7 @@ namespace ImageSharp.Tests.Drawing { image .BackgroundColor(Color.Blue) - .FillPolygon(Color.HotPink, simplePath) + .FillPolygon(Color.HotPink, simplePath, new GraphicsOptions(true)) .Save(output); }