Merge pull request #43 from woutware/Issue-572

Fixed single-precision InternalPath.FindIntersection precision problem for bitmap sizes > 1500 by using double-precision for intermediate calculations.
This commit is contained in:
Scott Williams 2018-05-20 11:01:49 +01:00 коммит произвёл GitHub
Родитель f5285d8699 9f379d9662
Коммит b36f00be55
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
2 изменённых файлов: 181 добавлений и 55 удалений

Просмотреть файл

@ -528,7 +528,14 @@ namespace SixLabors.Shapes
Vector2 line2Start = target.Start;
Vector2 line2End = target.End;
float x1, y1, x2, y2, x3, y3, x4, y4;
// Use double precision for the intermediate calculations, because single precision calculations
// easily gets over the Epsilon2 threshold for bitmap sizes larger than about 1500.
// This is still symptom fighting though, and probably the intersection finding algorithm
// should be looked over in the future (making the segments fat using epsilons doesn't truely fix the
// robustness problem).
// Future potential improvement: the precision problem will be reduced if the center of the bitmap is used as origin (0, 0),
// this will keep coordinates smaller and relatively precision will be larger.
double x1, y1, x2, y2, x3, y3, x4, y4;
x1 = line1Start.X;
y1 = line1Start.Y;
x2 = line1End.X;
@ -539,23 +546,23 @@ namespace SixLabors.Shapes
x4 = line2End.X;
y4 = line2End.Y;
float x12 = x1 - x2;
float y12 = y1 - y2;
float x34 = x3 - x4;
float y34 = y3 - y4;
float inter = (x12 * y34) - (y12 * x34);
double x12 = x1 - x2;
double y12 = y1 - y2;
double x34 = x3 - x4;
double y34 = y3 - y4;
double inter = (x12 * y34) - (y12 * x34);
if (inter > -Epsilon && inter < Epsilon)
{
return MaxVector;
}
float u = (x1 * y2) - (x2 * y1);
float v = (x3 * y4) - (x4 * y3);
float x = ((x34 * u) - (x12 * v)) / inter;
float y = ((y34 * u) - (y12 * v)) / inter;
double u = (x1 * y2) - (x2 * y1);
double v = (x3 * y4) - (x4 * y3);
double x = ((x34 * u) - (x12 * v)) / inter;
double y = ((y34 * u) - (y12 * v)) / inter;
Vector2 point = new Vector2(x, y);
Vector2 point = new Vector2((float)x, (float)y);
if (IsOnSegments(source, target, point))
{

Просмотреть файл

@ -1,17 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using SixLabors.Primitives;
using System.Buffers;
using Xunit.Abstractions;
// ReSharper disable InconsistentNaming
namespace SixLabors.Shapes.Tests
{
using SixLabors.Primitives;
using System.Buffers;
using System.Globalization;
using System.Numerics;
public class ComplexPolygonTests
{
public static readonly TheoryData<int, int, int> CommonOffsetData = new TheoryData<int, int, int>()
{
{0, 0, 0},
{1500, 1500, 0},
{3000, 3000, 0},
{8000, 8000, 0},
{20000, 20000, 0},
{0, 0, 42},
{1500, 1500, 42},
{3000, 3000, 42},
{8000, 8000, 42},
{20000, 20000, 42},
{0, 0, 123},
{1500, 1500, 123},
{3000, 3000, 123},
{8000, 8000, 123},
{20000, 20000, 123},
};
public ComplexPolygonTests(ITestOutputHelper output)
{
this.Output = output;
}
private ITestOutputHelper Output { get; }
[Fact]
public void MissingIntersection()
{
@ -26,17 +54,17 @@ namespace SixLabors.Shapes.Tests
new PointF(37, 85),
new PointF(93, 85)));
int intersections1 = ScanY(hole1, 137, data, 6, 0);
int intersections1 = this.ScanY(hole1, 137, data, 6, 0);
Assert.Equal(2, intersections1);
IPath poly = simplePath.Clip(hole1);
int intersections = ScanY(poly, 137, data, 6, 0);
int intersections = this.ScanY(poly, 137, data, 6, 0);
// returns an even number of points
Assert.Equal(4, intersections);
}
public int ScanY(IPath shape, int y, float[] buffer, int length, int offset)
public int ScanY(IPath shape, float y, float[] buffer, int length, int offset)
{
PointF start = new PointF(shape.Bounds.Left - 1, y);
PointF end = new PointF(shape.Bounds.Right + 1, y);
@ -58,58 +86,149 @@ namespace SixLabors.Shapes.Tests
}
}
[Fact]
public void MissingIntersectionsOpenSans_a()
[Theory]
[MemberData(nameof(CommonOffsetData))]
public void MissingIntersectionsOpenSans_a(int dx, int dy, int noiseSeed)
{
string path = @"36.57813x49.16406 35.41797x43.67969 35.41797x43.67969 35.13672x43.67969 35.13672x43.67969 34.41629x44.54843 33.69641x45.34412 32.97708x46.06674 32.2583x46.71631 31.54007x47.29282 30.82239x47.79626 30.10526x48.22665 29.38867x48.58398 29.38867x48.58398 28.65012x48.88474 27.86707x49.14539 27.03952x49.36594 26.16748x49.54639 25.25095x49.68674 24.28992x49.78699 23.28439x49.84714 22.23438x49.86719 22.23438x49.86719 21.52775x49.85564 20.84048x49.82104 20.17258x49.76337 19.52405x49.68262 18.28506x49.4519 17.12354x49.12891 16.03946x48.71362 15.03284x48.20605 14.10367x47.6062 13.25195x46.91406 13.25195x46.91406 12.48978x46.13678 11.82922x45.28149 11.27029x44.34821 10.81299x43.33691 10.45731x42.24762 10.20325x41.08032 10.05081x39.83502 10.0127x39.18312 10x38.51172 10x38.51172 10.01823x37.79307 10.07292x37.09613 10.16407x36.42088 10.29169x35.76733 10.6563x34.52533 11.16675x33.37012 11.82304x32.3017 12.62518x31.32007 13.57317x30.42523 14.10185x30.01036 14.66699x29.61719 15.2686x29.24571 15.90666x28.89594 16.58119x28.56786 17.29218x28.26147 18.03962x27.97679 18.82353x27.71381 19.6439x27.47252 20.50073x27.25293 22.32378x26.87885 24.29266x26.59155 26.40739x26.39105 28.66797x26.27734 28.66797x26.27734 35.20703x26.06641 35.20703x26.06641 35.20703x23.67578 35.20703x23.67578 35.17654x22.57907 35.08508x21.55652 34.93265x20.60812 34.71924x19.73389 34.44485x18.93381 34.1095x18.20789 33.71317x17.55612 33.25586x16.97852 33.25586x16.97852 32.73154x16.47177 32.13416x16.03259 31.46371x15.66098 30.72021x15.35693 29.90366x15.12045 29.01404x14.95154 28.05136x14.85019 27.01563x14.81641 27.01563x14.81641 25.79175x14.86255 24.52832x15.00098 23.88177x15.1048 23.22534x15.23169 21.88281x15.55469 20.50073x15.96997 19.0791x16.47754 17.61792x17.07739 16.11719x17.76953 16.11719x17.76953 14.32422x13.30469 14.32422x13.30469 15.04465x12.92841 15.7821x12.573 17.30811x11.9248 18.90222x11.36011 20.56445x10.87891 20.56445x10.87891 22.26184x10.49438 23.96143x10.21973 24.81204x10.1236 25.66321x10.05493 26.51492x10.01373 27.36719x10 27.36719x10 29.03409x10.04779 29.82572x10.10753 30.58948x10.19116 31.32536x10.29869 32.03336x10.43011 32.71348x10.58543 33.36572x10.76465 34.58658x11.19476 35.69592x11.72046 36.69376x12.34174 37.58008x13.05859 37.58008x13.05859 38.35873x13.88092 39.03357x14.8186 39.60458x15.87164 40.07178x17.04004 40.26644x17.6675 40.43515x18.32379 40.5779x19.00893 40.6947x19.7229 40.78555x20.46571 40.85043x21.23737 40.88937x22.03786 40.90234x22.86719 40.90234x22.86719 40.90234x49.16406
23.39453x45.05078 24.06655x45.03911 24.72031x45.00409 25.97302x44.86401 27.15268x44.63055 28.25928x44.30371 29.29282x43.88348 30.2533x43.36987 31.14072x42.76288 31.95508x42.0625 31.95508x42.0625 32.6843x41.27808 33.31628x40.41895 33.85104x39.48511 34.28857x38.47656 34.62888x37.39331 34.87195x36.23535 35.01779x35.00269 35.06641x33.69531 35.06641x33.69531 35.06641x30.21484 35.06641x30.21484 29.23047x30.46094 29.23047x30.46094 27.55093x30.54855 25.9928x30.68835 24.55606x30.88034 23.24072x31.12451 22.04678x31.42087 20.97424x31.76941 20.0231x32.17014 19.19336x32.62305 19.19336x32.62305 18.47238x33.13528 17.84753x33.71399 17.31882x34.35916 16.88623x35.0708 16.54977x35.84891 16.30945x36.69348 16.16525x37.60452 16.11719x38.58203 16.11719x38.58203 16.14713x39.34943 16.23694x40.06958 16.38663x40.74249 16.59619x41.36816 17.19495x42.47778 18.0332x43.39844 18.0332x43.39844 19.08679x44.12134 19.68527x44.40533 20.33154x44.6377 21.0256x44.81842 21.76746x44.94751 22.5571x45.02496 23.39453x45.05078";
string[] paths = path.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
Polygon[] polys = paths.Select(line => {
string[] pl = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
PointF[] points = pl.Select(p => p.Split('x'))
.Select(p => {
return new PointF(float.Parse(p[0]), float.Parse(p[1]));
})
.ToArray();
return new Polygon(new LinearLineSegment(points));
}).ToArray();
ComplexPolygon complex = new ComplexPolygon(polys);
float[] data = new float[complex.MaxIntersections];
int intersections = ScanY(complex, 10, data, complex.MaxIntersections, 0);
Vector2 offset = MakeOffsetVector(dx, dy, noiseSeed);
this.TestMissingFontIntersectionCore(offset, path, 10,
(intersections, data) =>
{
Assert.True(intersections % 2 == 0, $"even number of intersections expected but found {intersections}");
});
}
[Fact]
public void MissingIntersectionsOpenSans_o()
[Theory]
[MemberData(nameof(CommonOffsetData))]
public void MissingIntersectionsOpenSans_o(int dx, int dy, int noiseSeed)
{
string path = @"45.40234x29.93359 45.3838x31.09519 45.32819x32.22452 45.23549x33.32157 45.10571x34.38635 44.93886x35.41886 44.73492x36.4191 44.49391x37.38706 44.21582x38.32275 43.90065x39.22617 43.5484x40.09732 43.15907x40.9362 42.73267x41.7428 42.26918x42.51713 41.76862x43.25919 41.23097x43.96897 40.65625x44.64648 40.65625x44.64648 40.04884x45.28719 39.41315x45.88657 38.74916x46.4446 38.05688x46.9613 37.33632x47.43667 36.58746x47.8707 35.81032x48.26339 35.00488x48.61475 34.17116x48.92477 33.30914x49.19345 32.41884x49.4208 31.50024x49.60681 30.55336x49.75149 29.57819x49.85483 28.57472x49.91683 27.54297x49.9375 27.54297x49.9375 26.2691x49.8996 25.03149x49.78589 23.83014x49.59637 22.66504x49.33105 21.53619x48.98993 20.4436x48.573 19.38727x48.08026 18.36719x47.51172 18.36719x47.51172 17.3938x46.87231 16.47754x46.16699 15.61841x45.39575 14.81641x44.55859 14.07153x43.65552 13.38379x42.68652 12.75317x41.65161 12.17969x40.55078 12.17969x40.55078 11.66882x39.39282 11.22607x38.18652 10.85144x36.93188 10.54492x35.62891 10.30652x34.27759 10.13623x32.87793 10.03406x31.42993 10x29.93359 10x29.93359 10.0184x28.77213 10.07361x27.64322 10.16562x26.54685 10.29443x25.48303 10.46005x24.45176 10.66248x23.45303 10.9017x22.48685 11.17773x21.55322 11.49057x20.65214 11.84021x19.7836 12.22665x18.94761 12.6499x18.14417 13.10995x17.37327 13.60681x16.63492 14.14047x15.92912 14.71094x15.25586 14.71094x15.25586 15.31409x14.61941 15.9458x14.02402 16.60608x13.46969 17.29492x12.95642 18.01233x12.48421 18.7583x12.05307 19.53284x11.66299 20.33594x11.31396 21.1676x11.006 22.02783x10.73911 22.91663x10.51327 23.83398x10.32849 24.77991x10.18478 25.75439x10.08212 26.75745x10.02053 27.78906x10 27.78906x10 28.78683x10.02101 29.75864x10.08405 30.70449x10.1891 31.62439x10.33618 32.51833x10.52528 33.38632x10.75641 34.22836x11.02956 35.04443x11.34473 35.83456x11.70192 36.59872x12.10114 37.33694x12.54237 38.04919x13.02563 38.7355x13.55092 39.39584x14.11823 40.03024x14.72755 40.63867x15.37891 40.63867x15.37891 41.21552x16.0661 41.75516x16.78296 42.25757x17.52948 42.72278x18.30566 43.15077x19.11151 43.54153x19.94702 43.89509x20.81219 44.21143x21.70703 44.49055x22.63153 44.73245x23.58569 44.93714x24.56952 45.10461x25.58301 45.23487x26.62616 45.32791x27.69897 45.38374x28.80145 45.40234x29.93359
16.04688x29.93359 16.09302x31.72437 16.23145x33.40527 16.33527x34.20453 16.46216x34.97632 16.61212x35.72064 16.78516x36.4375 16.98126x37.12689 17.20044x37.78882 17.44269x38.42328 17.70801x39.03027 18.30786x40.16187 19x41.18359 19x41.18359 19.78168x42.08997 20.65015x42.87549 21.60541x43.54016 22.64746x44.08398 23.77631x44.50696 24.99194x44.80908 26.29437x44.99036 26.97813x45.03568 27.68359x45.05078 27.68359x45.05078 28.38912x45.03575 29.07309x44.99063 30.37634x44.81018 31.59335x44.50943 32.72412x44.08838 33.76865x43.54703 34.72693x42.88538 35.59897x42.10342 36.38477x41.20117 36.38477x41.20117 37.08102x40.18301 37.68445x39.05334 37.95135x38.44669 38.19504x37.81216 38.41552x37.14976 38.61279x36.45947 38.78686x35.74131 38.93771x34.99527 39.06536x34.22135 39.1698x33.41956 39.30905x31.73233 39.35547x29.93359 39.35547x29.93359 39.30905x28.15189 39.1698x26.48059 39.06536x25.68635 38.93771x24.91971 38.78686x24.18067 38.61279x23.46924 38.41552x22.78541 38.19504x22.12918 37.95135x21.50056 37.68445x20.89954 37.08102x19.7803 36.38477x18.77148 36.38477x18.77148 35.59787x17.87747 34.72253x17.10266 33.75876x16.44705 32.70654x15.91064 31.56589x15.49344 30.33679x15.19543 29.68908x15.09113 29.01926x15.01663 28.32732x14.97193 27.61328x14.95703 27.61328x14.95703 26.90796x14.97173 26.22461x15.01581 24.92383x15.19214 23.71094x15.48602 22.58594x15.89746 21.54883x16.42645 20.59961x17.073 19.73828x17.8371 18.96484x18.71875 18.96484x18.71875 18.28094x19.71686 17.68823x20.83032 17.42607x21.43031 17.18671x22.05914 16.97014x22.71681 16.77637x23.40332 16.60539x24.11867 16.45721x24.86285 16.33183x25.63588 16.22925x26.43774 16.09247x28.12799 16.04688x29.93359 ";
Vector2 offset = MakeOffsetVector(dx, dy, noiseSeed);
this.TestMissingFontIntersectionCore(offset, path, 30,
(intersections, data) =>
{
float expectedMinX = 28f + offset.X;
Assert.True(data[1] < expectedMinX, $"second intersection should be > {expectedMinX} but was {data[1]}");
Assert.True(intersections % 2 == 0, $"even number of intersections expected but found {intersections}");
});
}
private void TestMissingFontIntersectionCore(Vector2 offset, string path, int scanStartY, Action<int, float[]> validateIntersections)
{
string[] paths = path.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
Polygon[] polys = paths.Select(line => {
string[] pl = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
PointF[] points = pl.Select(p => p.Split('x'))
.Select(p => {
return new PointF(float.Parse(p[0]), float.Parse(p[1]));
.Select(p =>
{
float x = float.Parse(p[0], CultureInfo.InvariantCulture);
float y = float.Parse(p[1], CultureInfo.InvariantCulture);
PointF pt = new Vector2(x, y) + offset;
return pt;
})
.ToArray();
return new Polygon(new LinearLineSegment(points));
}).ToArray();
ComplexPolygon complex = new ComplexPolygon(polys);
float[] data = new float[complex.MaxIntersections];
int intersections = ScanY(complex, 30, data, complex.MaxIntersections, 0);
int intersections = this.ScanY(complex, scanStartY + offset.Y, data, complex.MaxIntersections, 0);
Assert.True(data[1] < 28, $"second intersection should be > 28 but was {data[1]}");
Assert.True(intersections % 2 == 0, $"even number of intersections expected but found {intersections}");
validateIntersections(intersections, data);
}
/// <summary>
/// Test is based on @woutware's the idea and drawing logic in opening comment:
/// https://github.com/SixLabors/Shapes/pull/43#issue-189141926
/// </summary>
[Theory]
[InlineData(100, 100, 10, 3)]
[InlineData(800, 600, 8, 2)]
[InlineData(1500, 1500, 10, 3)]
[InlineData(4000, 4000, 8, 2)]
public void SmallRingAtLargeCoords_HorizontalScansShouldFind4IntersectionPoints(int w, int h, int r, int thickness)
{
int cx = w - 2 * r;
int cy = h - 2 * 3;
EllipsePolygon ellipse = new EllipsePolygon(cx, cy, r);
IPath path = ellipse.GenerateOutline(thickness);
int yMin = cy - r + thickness + 1;
int yMax = cy + r - thickness;
PointF[] buffer = new PointF[16];
List<int> badPositions = new List<int>();
for (int y = yMin; y < yMax; y++)
{
PointF start = new PointF(-1, y);
PointF end = new PointF(w + 1, y);
int intersectionCount = path.FindIntersections(start, end, buffer);
if (intersectionCount != 4)
{
badPositions.Add(y);
}
}
if (badPositions.Any())
{
string badPoz = string.Join(',', badPositions);
this.Output.WriteLine($"BAD: {badPositions.Count} of {yMax - yMin}: {badPoz}");
Assert.True(false);
}
}
/// <summary>
/// Test is based on @woutware's the idea in another issue comment:
/// https://github.com/SixLabors/Shapes/pull/43#issuecomment-390358702
/// </summary>
[Theory]
[MemberData(nameof(CommonOffsetData))]
public void OffsetingIntersectingSegments_ShouldPreserveIntersection(int dx, int dy, int noiseSeed)
{
Vector2 offset = MakeOffsetVector(dx, dy, noiseSeed);
PointF a = new Vector2(21.904f, 78.48f) + offset;
PointF b = new Vector2(22.026f, 79.8f) + offset;
PointF c = new Vector2(48f, 78.5f) + offset;
PointF d = new Vector2(20f, 78.5f) + offset;
Path path = new Path(new LinearLineSegment(a, b));
int count = path.FindIntersections(c, d, new PointF[1]);
Assert.Equal(1, count);
}
private static Vector2 MakeOffsetVector(int dx, int dy, int noiseSeed)
{
Vector2 offset = new Vector2(dx, dy);
// Let's randomize the input data, while still keeping the test reproducible
if (noiseSeed > 0)
{
Random rnd = new Random(noiseSeed);
offset.X += (float)rnd.NextDouble();
offset.Y += (float)rnd.NextDouble();
}
return offset;
}
}
}