diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs index df0f3a2647..ea688a65dc 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Graphics; using osuTK.Graphics; using osu.Framework.Graphics.Containers; @@ -18,6 +19,7 @@ public partial class TestSceneInteractivePathDrawing : FrameworkTestScene { private readonly Path rawDrawnPath; private readonly Path approximatedDrawnPath; + private readonly Path controlPointPath; private readonly Container controlPointViz; private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder(); @@ -39,6 +41,12 @@ public TestSceneInteractivePathDrawing() Colour = Color4.Blue, PathRadius = 3, }, + controlPointPath = new Path + { + Colour = Color4.LightGreen, + PathRadius = 1, + Alpha = 0.5f, + }, controlPointViz = new Container { RelativeSizeAxes = Axes.Both, @@ -55,11 +63,11 @@ public TestSceneInteractivePathDrawing() bSplineBuilder.Clear(); }); - AddSliderStep($"{nameof(bSplineBuilder.Degree)}", 1, 5, 3, v => + AddSliderStep($"{nameof(bSplineBuilder.Degree)}", 1, 4, 3, v => { bSplineBuilder.Degree = v; }); - AddSliderStep($"{nameof(bSplineBuilder.Tolerance)}", 0f, 3f, 1.5f, v => + AddSliderStep($"{nameof(bSplineBuilder.Tolerance)}", 0f, 3f, 2f, v => { bSplineBuilder.Tolerance = v; }); @@ -71,17 +79,21 @@ public TestSceneInteractivePathDrawing() private void updateControlPointsViz() { + controlPointPath.Vertices = bSplineBuilder.ControlPoints.SelectMany(o => o).ToArray(); controlPointViz.Clear(); - foreach (var cp in bSplineBuilder.ControlPoints) + foreach (var segment in bSplineBuilder.ControlPoints) { - controlPointViz.Add(new Box + foreach (var cp in segment) { - Origin = Anchor.Centre, - Size = new Vector2(10), - Position = cp, - Colour = Color4.LightGreen, - }); + controlPointViz.Add(new Box + { + Origin = Anchor.Centre, + Size = new Vector2(10), + Position = cp, + Colour = Color4.LightGreen, + }); + } } } @@ -109,5 +121,13 @@ protected override void OnDrag(DragEvent e) { bSplineBuilder.AddLinearPoint(rawDrawnPath.ToLocalSpace(ToScreenSpace(e.MousePosition))); } + + protected override void OnDragEnd(DragEndEvent e) + { + if (e.Button == MouseButton.Left) + bSplineBuilder.Finish(); + + base.OnDragEnd(e); + } } } diff --git a/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs b/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs index b6a21c7459..cd4cad5631 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osu.Framework.Testing; @@ -32,7 +34,6 @@ public TestScenePiecewiseLinearToBSpline() Cell(0).AddRange(new[] { createLabel(nameof(PathApproximator.BezierToPiecewiseLinear)), - new ApproximatedPathTest(PathApproximator.BezierToPiecewiseLinear), doubleApproximatedPathTests[^1], }); @@ -40,7 +41,6 @@ public TestScenePiecewiseLinearToBSpline() Cell(1).AddRange(new[] { createLabel(nameof(PathApproximator.CatmullToPiecewiseLinear)), - new ApproximatedPathTest(PathApproximator.CatmullToPiecewiseLinear), doubleApproximatedPathTests[^1], }); @@ -48,7 +48,6 @@ public TestScenePiecewiseLinearToBSpline() Cell(2).AddRange(new[] { createLabel(nameof(PathApproximator.CircularArcToPiecewiseLinear)), - new ApproximatedPathTest(PathApproximator.CircularArcToPiecewiseLinear), doubleApproximatedPathTests[^1], }); @@ -56,7 +55,6 @@ public TestScenePiecewiseLinearToBSpline() Cell(3).AddRange(new[] { createLabel(nameof(PathApproximator.LagrangePolynomialToPiecewiseLinear)), - new ApproximatedPathTest(PathApproximator.LagrangePolynomialToPiecewiseLinear), doubleApproximatedPathTests[^1], }); @@ -134,26 +132,7 @@ private void updateTests() public delegate List ApproximatorFunc(ReadOnlySpan controlPoints); - private partial class ApproximatedPathTest : SmoothPath - { - public ApproximatedPathTest(ApproximatorFunc approximator) - { - Vector2[] points = new Vector2[5]; - points[0] = new Vector2(50, 250); - points[1] = new Vector2(150, 230); - points[2] = new Vector2(100, 150); - points[3] = new Vector2(200, 80); - points[4] = new Vector2(250, 50); - - AutoSizeAxes = Axes.None; - RelativeSizeAxes = Axes.Both; - PathRadius = 2; - Vertices = approximator(points); - Colour = Color4.White; - } - } - - private partial class DoubleApproximatedPathTest : SmoothPath + private partial class DoubleApproximatedPathTest : Container { private readonly Vector2[] inputPath; @@ -173,6 +152,10 @@ private partial class DoubleApproximatedPathTest : SmoothPath public bool OptimizePath { get; set; } + private readonly Path approximatedDrawnPath; + private readonly Path controlPointPath; + private readonly Container controlPointViz; + public DoubleApproximatedPathTest(ApproximatorFunc approximator) { Vector2[] points = new Vector2[5]; @@ -184,9 +167,33 @@ public DoubleApproximatedPathTest(ApproximatorFunc approximator) AutoSizeAxes = Axes.None; RelativeSizeAxes = Axes.Both; - PathRadius = 2; - Colour = Color4.Magenta; inputPath = approximator(points).ToArray(); + + Children = new Drawable[] + { + new Path + { + Colour = Color4.White, + PathRadius = 2, + Vertices = inputPath, + }, + approximatedDrawnPath = new Path + { + Colour = Color4.Magenta, + PathRadius = 2, + }, + controlPointPath = new Path + { + Colour = Color4.LightGreen, + PathRadius = 1, + Alpha = 0.5f, + }, + controlPointViz = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.5f, + }, + }; } public void UpdatePath() @@ -194,7 +201,20 @@ public void UpdatePath() if (!OptimizePath) return; var controlPoints = PathApproximator.PiecewiseLinearToBSpline(inputPath, NumControlPoints, Degree, NumTestPoints, MaxIterations, LearningRate, B1, B2); - Vertices = PathApproximator.BSplineToPiecewiseLinear(controlPoints.ToArray(), Degree); + approximatedDrawnPath.Vertices = PathApproximator.BSplineToPiecewiseLinear(controlPoints.ToArray(), Degree); + controlPointPath.Vertices = controlPoints; + controlPointViz.Clear(); + + foreach (var cp in controlPoints) + { + controlPointViz.Add(new Box + { + Origin = Anchor.Centre, + Size = new Vector2(10), + Position = cp, + Colour = Color4.LightGreen, + }); + } } } } diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index ee48292459..ad773bc69d 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using osu.Framework.Caching; using osu.Framework.Graphics.Primitives; using osuTK; @@ -75,11 +74,15 @@ private static float getAbsWindingAt(List path, List cumulativeD Value = new List() }; - private readonly Cached> controlPoints = new Cached> + private readonly Cached>> controlPoints = new Cached>> { - Value = new List() + Value = new List>() }; + private bool shouldOptimiseLastSegment; + + private bool finishedDrawing; + private int degree; /// @@ -166,12 +169,15 @@ public IReadOnlyList OutputPath /// /// The list of control points of the B-Spline. This is inferred from the input path. /// - public IReadOnlyList ControlPoints + public IReadOnlyList> ControlPoints { get { if (!controlPoints.IsValid) - regenerateApproximatedPathControlPoints(); + regenerateFullApproximatedPath(); + + if (shouldOptimiseLastSegment) + regenerateLastApproximatedSegment(); return controlPoints.Value; } @@ -273,7 +279,166 @@ private List detectCorners(List vertices, List distances) return cornerT; } - private void regenerateApproximatedPathControlPoints() + private (List, List, float) initializeSegment(List vertices, List distances, float t0, float t1) + { + // Populate each segment between corners with control points that have density proportional to the + // product of Tolerance and curvature. + const float step_size = FD_EPSILON; + + float totalWinding = 0; + + Vector2 c0 = getPathAt(vertices, distances, t0); + Vector2 c1 = getPathAt(vertices, distances, t1); + Line linearConnection = new Line(c0, c1); + + t0 += step_size * 2; + t1 -= step_size * 2; + + var cps = new List { c0 }; + var segmentPath = new List { c0 }; + bool allOnLine = true; + float onLineThreshold = 0.02f * Tolerance * Vector2.Distance(c0, c1); + + if (t1 > t0) + { + int nSteps = (int)((t1 - t0) / step_size); + float currentWinding = 0.5f * Tolerance; + + for (int j = 0; j < nSteps; ++j) + { + float t = t0 + j * step_size; + float winding = getAbsWindingAt(vertices, distances, t); + totalWinding += winding; + currentWinding += winding; + + Vector2 p = getPathAt(vertices, distances, t); + segmentPath.Add(p); + + if (currentWinding < Tolerance) continue; + + if (linearConnection.DistanceSquaredToPoint(p) > onLineThreshold * onLineThreshold) + allOnLine = false; + + cps.Add(p); + currentWinding -= Tolerance; + } + } + + cps.Add(c1); + segmentPath.Add(c1); + + return allOnLine ? (new List { c0, c1 }, segmentPath, totalWinding) : (cps, segmentPath, totalWinding); + } + + private void updateLastSegment(List vertices, List distances, List cornerTs, List> segments, int iterations, bool mask) + { + if (segments.Count == 0 || segments.Count >= cornerTs.Count) return; + + // Initialize control points for the last segment + int i = segments.Count - 1; + var lastSegment = segments[i]; + var (cps, segmentPath, totalWinding) = initializeSegment(vertices, distances, cornerTs[i], cornerTs[i + 1]); + + // Make sure the last segment has the correct end-points + if (lastSegment.Count >= 1) + lastSegment[0] = cps[0]; + else + lastSegment.Add(cps[0]); + if (lastSegment.Count >= 2) + lastSegment[^1] = cps[^1]; + else + lastSegment.Add(cps[^1]); + + // Make sure the last segment has the correct number of control points. + if (cps.Count > lastSegment.Count) + { + int toAdd = cps.Count - lastSegment.Count; + + for (int j = 0; j < toAdd; j++) + { + lastSegment.Insert(lastSegment.Count - 1, cps[j + cps.Count - toAdd - 1]); + } + } + else if (cps.Count < lastSegment.Count) + { + int toRemove = lastSegment.Count - cps.Count; + lastSegment.RemoveRange(lastSegment.Count - toRemove - 1, toRemove); + } + + // Optimize the control point placement + if (lastSegment.Count <= 2 || lastSegment.Count >= 100) return; + + float[,]? learnableMask = null; + + if (mask) + { + // When live-drawing, only the end of the segment gets extended in which case we use this mask to make updates less wobbly. + // Make a mask to prevent modifying the control points which have barely any impact on the end of the segment. + // Also the end-points can not move. + learnableMask = new float[2, lastSegment.Count]; + + // Only the 2 * degree last control points are not fixed in place. + // This number was chosen because manual testing showed that control points outside this range barely get moved + // by the optimization when the end of the segment gets extended. + for (int j = Math.Max(1, lastSegment.Count - degree * 2); j < lastSegment.Count - 1; j++) + { + learnableMask[0, j] = 1; + learnableMask[1, j] = 1; + } + } + + int res = (int)(totalWinding * 10); + segments[^1] = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), lastSegment.Count, degree, + res, iterations, 4f, initialControlPoints: lastSegment, learnableMask: learnableMask); + } + + private void regenerateLastApproximatedSegment() + { + if (!controlPoints.IsValid) + { + regenerateFullApproximatedPath(); + return; + } + + var (vertices, distances) = computeSmoothedInputPath(); + + if (vertices.Count < 2) + { + controlPoints.Value = new List> { vertices }; + return; + } + + Debug.Assert(vertices.Count == distances.Count + 1); + var cornerTs = detectCorners(vertices, distances); + + var segments = controlPoints.Value; + + // In some extremely rare cases there can be less corners than on the previous frame, + // so we have to remove the last segments if that is the case. + if (segments.Count >= cornerTs.Count) + { + int toRemove = segments.Count + 1 - cornerTs.Count; + segments.RemoveRange(segments.Count - toRemove, toRemove); + } + + // Make sure there are enough segments. + while (segments.Count < cornerTs.Count - 1) + { + // The previous segment may have been shortened by the addition of a corner. + // We have to remove the extra control points and re-optimize the path. + updateLastSegment(vertices, distances, cornerTs, segments, 100, false); + segments.Add(new List()); + } + + if (finishedDrawing) + updateLastSegment(vertices, distances, cornerTs, segments, 100, false); + else + updateLastSegment(vertices, distances, cornerTs, segments, 10, true); + + shouldOptimiseLastSegment = false; + } + + private void regenerateFullApproximatedPath() { // Approximating a given input path with a BSpline has three stages: // 1. Fit a dense-ish BSpline (with one control point in FdEpsilon-sized intervals) to the input path. @@ -288,90 +453,42 @@ private void regenerateApproximatedPathControlPoints() if (vertices.Count < 2) { - controlPoints.Value = vertices; + controlPoints.Value = new List> { vertices }; return; } - controlPoints.Value = new List(); + controlPoints.Value = new List>(); Debug.Assert(vertices.Count == distances.Count + 1); var cornerTs = detectCorners(vertices, distances); - var cps = controlPoints.Value; - cps.Add(vertices[0]); - - // Populate each segment between corners with control points that have density proportional to the - // product of Tolerance and curvature. - const float step_size = FD_EPSILON; + var segments = controlPoints.Value; for (int i = 1; i < cornerTs.Count; ++i) { - float totalWinding = 0; - - float t0 = cornerTs[i - 1] + step_size * 2; - float t1 = cornerTs[i] - step_size * 2; - - Vector2 c0 = getPathAt(vertices, distances, cornerTs[i - 1]); - Vector2 c1 = getPathAt(vertices, distances, cornerTs[i]); - Line linearConnection = new Line(c0, c1); + var (cps, segmentPath, totalWinding) = initializeSegment(vertices, distances, cornerTs[i - 1], cornerTs[i]); - var tmp = new List(); - bool allOnLine = true; - float onLineThreshold = 5 * Tolerance * step_size; - - if (t1 > t0) + if (cps.Count > 2 && cps.Count < 100) { - int nSteps = (int)((t1 - t0) / step_size); - - for (int j = 0; j < nSteps; ++j) - { - float t = t0 + j * step_size; - totalWinding += getAbsWindingAt(vertices, distances, t); - } - - float currentWinding = 0; - - for (int j = 0; j < nSteps; ++j) - { - float t = t0 + j * step_size; - - // Don't permit control points too close to the next corner as they are redundant. - // However, ignore this limitation on the last segment of the path as we would like - // it to follow the user's drawing as closely as possible. - if (currentWinding + tmp.Count * Tolerance > totalWinding - Tolerance * 0.5f && i < cornerTs.Count - 1) - break; - - if (currentWinding > Tolerance) - { - Vector2 p = getPathAt(vertices, distances, t); - if (linearConnection.DistanceSquaredToPoint(p) > onLineThreshold * onLineThreshold) - allOnLine = false; - - tmp.Add(p); - currentWinding -= Tolerance; - } - - currentWinding += getAbsWindingAt(vertices, distances, t); - } + int res = (int)(totalWinding * 10); + cps = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), cps.Count, degree, + res, 200, 5, initialControlPoints: cps); } - if (!allOnLine) - cps.AddRange(tmp); - - // Insert the corner at the end of the segment as a sharp control point consisting of - // degree many regular control points, meaning that the BSpline will have a kink here. - // Special case the last corner which will be the end of the path and thus automatically - // duplicated degree times by BSplineToPiecewiseLinear down the line. - if (i == cornerTs.Count - 1) - cps.Add(c1); - else - cps.AddRange(Enumerable.Repeat(c1, degree)); + segments.Add(cps); } + + shouldOptimiseLastSegment = false; } private void redrawApproximatedPath() { - outputCache.Value = PathApproximator.BSplineToPiecewiseLinear(ControlPoints.ToArray(), degree); + outputCache.Value = new List(); + + foreach (var segment in ControlPoints) + { + outputCache.Value.AddRange(PathApproximator.BSplineToPiecewiseLinear(segment.ToArray(), degree)); + } } /// @@ -382,8 +499,11 @@ public void Clear() inputPath.Clear(); cumulativeInputPathLength.Clear(); - controlPoints.Value = new List(); + controlPoints.Value = new List>(); outputCache.Value = new List(); + + shouldOptimiseLastSegment = false; + finishedDrawing = false; } /// @@ -393,7 +513,7 @@ public void Clear() public void AddLinearPoint(Vector2 v) { outputCache.Invalidate(); - controlPoints.Invalidate(); + shouldOptimiseLastSegment = true; // Implementation detail: we would like to disregard input path detail that is smaller than // FD_EPSILON * 2 because it can otherwise mess with the winding calculations. However, we @@ -421,5 +541,18 @@ public void AddLinearPoint(Vector2 v) inputPath.Add(v); cumulativeInputPathLength.Add(cumulativeInputPathLength[^1]); } + + /// + /// Call this when you are done building the path. + /// This method applies the final step of post-processing. + /// + public void Finish() + { + // Do additional optimization steps on the entire last segment. + // This improves results after drawing, so the performance stays fast and control points dont wobble too much while drawing. + outputCache.Invalidate(); + shouldOptimiseLastSegment = true; + finishedDrawing = true; + } } } diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 1228f5c75d..21a2cfdfdb 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -309,6 +309,7 @@ public static List LagrangePolynomialToPiecewiseLinear(ReadOnlySpanThe B1 parameter for the Adam optimizer. Between 0 and 1. /// The B2 parameter for the Adam optimizer. Between 0 and 1. /// The initial bezier control points to use before optimization. The length of this list should be equal to . + /// Mask determining which control point positions are fixed and cannot be changed by the optimiser. /// A List of vectors representing the bezier control points. public static List PiecewiseLinearToBezier(ReadOnlySpan inputPath, int numControlPoints, @@ -317,9 +318,12 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP float learningRate = 8f, float b1 = 0.8f, float b2 = 0.99f, - List? initialControlPoints = null) + List? initialControlPoints = null, + float[,]? learnableMask = null) { - return piecewiseLinearToSpline(inputPath, generateBezierWeights(numControlPoints, numTestPoints), maxIterations, learningRate, b1, b2, initialControlPoints); + numTestPoints = Math.Max(numTestPoints, 3); + return piecewiseLinearToSpline(inputPath, generateBezierWeights(numControlPoints, numTestPoints), + maxIterations, learningRate, b1, b2, initialControlPoints, learnableMask); } /// @@ -334,6 +338,7 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP /// The B1 parameter for the Adam optimizer. Between 0 and 1. /// The B2 parameter for the Adam optimizer. Between 0 and 1. /// The initial B-spline control points to use before optimization. The length of this list should be equal to . + /// Mask determining which control point positions are fixed and cannot be changed by the optimiser. /// A List of vectors representing the B-spline control points. public static List PiecewiseLinearToBSpline(ReadOnlySpan inputPath, int numControlPoints, @@ -343,10 +348,13 @@ public static List PiecewiseLinearToBSpline(ReadOnlySpan input float learningRate = 8f, float b1 = 0.8f, float b2 = 0.99f, - List? initialControlPoints = null) + List? initialControlPoints = null, + float[,]? learnableMask = null) { degree = Math.Min(degree, numControlPoints - 1); - return piecewiseLinearToSpline(inputPath, generateBSplineWeights(numControlPoints, numTestPoints, degree), maxIterations, learningRate, b1, b2, initialControlPoints); + numTestPoints = Math.Max(numTestPoints, 3); + return piecewiseLinearToSpline(inputPath, generateBSplineWeights(numControlPoints, numTestPoints, degree), + maxIterations, learningRate, b1, b2, initialControlPoints, learnableMask); } /// @@ -360,6 +368,7 @@ public static List PiecewiseLinearToBSpline(ReadOnlySpan input /// The B1 parameter for the Adam optimizer. Between 0 and 1. /// The B2 parameter for the Adam optimizer. Between 0 and 1. /// The initial control points to use before optimization. The length of this list should be equal to the number of test points. + /// Mask determining which control point positions are fixed and cannot be changed by the optimiser. /// A List of vectors representing the spline control points. private static List piecewiseLinearToSpline(ReadOnlySpan inputPath, float[,] weights, @@ -367,7 +376,8 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input float learningRate = 8f, float b1 = 0.8f, float b2 = 0.99f, - List? initialControlPoints = null) + List? initialControlPoints = null, + float[,]? learnableMask = null) { int numControlPoints = weights.GetLength(1); int numTestPoints = weights.GetLength(0); @@ -405,12 +415,16 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input // Initialize Adam optimizer variables float[,] m = new float[2, numControlPoints]; float[,] v = new float[2, numControlPoints]; - float[,] learnableMask = new float[2, numControlPoints]; - for (int i = 1; i < numControlPoints - 1; i++) + if (learnableMask is null) { - learnableMask[0, i] = 1; - learnableMask[1, i] = 1; + learnableMask = new float[2, numControlPoints]; + + for (int i = 1; i < numControlPoints - 1; i++) + { + learnableMask[0, i] = 1; + learnableMask[1, i] = 1; + } } // Initialize intermediate variables @@ -425,7 +439,7 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input // Update labels to shift the distance distribution between points if (step % 11 == 0) { - getDistanceDistribution(points, distanceDistribution); + getDistanceDistribution(points, distanceDistribution, 0.1f); interpolator.Interpolate(distanceDistribution, labels); } @@ -434,7 +448,7 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input matmul(points, weightsTranspose, grad); matScale(grad, -1f / numControlPoints, grad); - // Apply learnable mask to prevent moving the endpoints + // Apply learnable mask to prevent moving the fixed points matProduct(grad, learnableMask, grad); // Update control points with Adam optimizer @@ -561,7 +575,8 @@ private static float[] linspace(float start, float end, int count) /// /// (2, n) shape array which represents the points of the piecewise-linear path. /// n-length array to write the result to. - private static void getDistanceDistribution(float[,] points, float[] result) + /// Factor to be added to each computed distance between points. + private static void getDistanceDistribution(float[,] points, float[] result, float regularizingFactor = 0f) { int m = points.GetLength(1); float accumulator = 0; @@ -570,7 +585,7 @@ private static void getDistanceDistribution(float[,] points, float[] result) for (int i = 1; i < m; i++) { float dist = MathF.Sqrt(MathF.Pow(points[0, i] - points[0, i - 1], 2) + MathF.Pow(points[1, i] - points[1, i - 1], 2)); - accumulator += dist; + accumulator += dist + regularizingFactor; result[i] = accumulator; }