/// <summary> /// Finds the minimum bounding circle /// </summary> /// <param name="points">The points.</param> /// <returns>System.Double.</returns> /// <exception cref="Exception">Bounding circle failed to converge</exception> /// <references> /// Based on Emo Welzl's "move-to-front heuristic" and this paper (algorithm 1). /// http://www.inf.ethz.ch/personal/gaertner/texts/own_work/esa99_final.pdf /// This algorithm runs in near linear time. Visiting most points just a few times. /// Though a linear algorithm was found by Meggi do, this algorithm is more robust /// (doesn't care about multiple points on a line and fewer rounding functions) /// and directly applicable to multiple dimensions (in our case, just 2 and 3 D). /// </references> public static BoundingCircle MinimumCircle(IList <PointLight> points) { #region Algorithm 1 ////Randomize the list of points //var r = new Random(); //var randomPoints = new List<PointLight>(points.OrderBy(p => r.Next())); //if (randomPoints.Count < 2) return new BoundingCircle(0.0, points[0]); ////Get any two points in the list points. //var point1 = randomPoints[0]; //var point2 = randomPoints[1]; //var previousPoints = new HashSet<PointLight>(); //var circle = new InternalCircle(point1, point2); //var stallCounter = 0; //var i = 0; //while (i < randomPoints.Count && stallCounter < points.Count * 2) //{ // var currentPoint = randomPoints[i]; // //If the current point is part of the circle or inside the circle, go to the next iteration // if (circle.Point0.Equals(currentPoint) || // circle.Point1.Equals(currentPoint) || // circle.Point2.Equals(currentPoint) || // circle.IsPointInsideCircle(currentPoint)) // { // i++; // continue; // } // //Else if the currentPoint is a previousPoint, increase dimension // if (previousPoints.Contains(currentPoint)) // { // //Make a new circle from the current two-point circle and the current point // circle = new InternalCircle(circle.Point0, circle.Point1, currentPoint); // previousPoints.Remove(currentPoint); // i++; // } // else // { // //Find the point in the circle furthest from new point. // circle.Furthest(currentPoint, out var furthestPoint, ref previousPoints); // //Make a new circle from the furthest point and current point // circle = new InternalCircle(currentPoint, furthestPoint); // //Add previousPoints to the front of the list // foreach (var previousPoint in previousPoints) // { // randomPoints.Remove(previousPoint); // randomPoints.Insert(0, previousPoint); // } // //Restart the search // stallCounter++; // i = 0; // } //} #endregion #region Algorithm 2: Furthest Point //var r = new Random(); //var randomPoints = new List<PointLight>(points.OrderBy(p => r.Next())); //Algorithm 2 //I tried using the extremes (X, Y, and also tried Sum, Diff) to do a first pass at the circle //or to define the starting circle for max dX or dY, but all of these were slower do to the extra //for loop at the onset. The current approach is faster and simpler; just start with some arbitrary points. var circle = new InternalCircle(points[0], points[points.Count / 2]); var dummyPoint = new PointLight(double.NaN, double.NaN); var stallCounter = 0; var successful = false; var stallLimit = points.Count * 1.5; if (stallLimit < 100) { stallLimit = 100; } var centerX = circle.CenterX; var centerY = circle.CenterY; var sqTolerance = Math.Sqrt(Constants.BaseTolerance); var sqRadiusPlusTolerance = circle.SqRadius + sqTolerance; var nextPoint = dummyPoint; var priorRadius = circle.SqRadius; while (!successful && stallCounter < stallLimit) { //If stallCounter is getting big, add a bit extra to the circle radius to ensure convergence if (stallCounter > stallLimit / 2) { sqRadiusPlusTolerance = sqRadiusPlusTolerance + Constants.BaseTolerance * sqRadiusPlusTolerance; } //Add it a second time if stallCounter is even bigger if (stallCounter > stallLimit * 2 / 3) { sqRadiusPlusTolerance = sqRadiusPlusTolerance + Constants.BaseTolerance * sqRadiusPlusTolerance; } //Find the furthest point from the center point var maxDistancePlusTolerance = sqRadiusPlusTolerance; var nextPointIsSet = false; foreach (var point in points) { var dx = centerX - point.X; var dy = centerY - point.Y; var squareDistanceToPoint = dx * dx + dy * dy; //If the square distance is less than or equal to the max square distance, continue. if (squareDistanceToPoint < maxDistancePlusTolerance) { continue; } //Otherwise, set this as the next point to go to. maxDistancePlusTolerance = squareDistanceToPoint + sqTolerance; nextPoint = point; nextPointIsSet = true; } if (!nextPointIsSet) { successful = true; continue; } //Create a new circle with 2 points //Find the point in the circle furthest from new point. circle.Furthest(nextPoint, out var furthestPoint, out var previousPoint1, out var previousPoint2, out var numPreviousPoints); //Make a new circle from the furthest point and current point circle = new InternalCircle(nextPoint, furthestPoint); centerX = circle.CenterX; centerY = circle.CenterY; sqRadiusPlusTolerance = circle.SqRadius + sqTolerance; //Now check if the previous points are outside this circle. //To be outside the circle, it must be further out than the specified tolerance. //Otherwise, the loop can get caught in a loop due to rounding error //If you wanted to use a tighter tolerance, you would need to take the square roots to get the radius. //If they are, increase the dimension and use three points in a circle var dxP1 = centerX - previousPoint1.X; var dyP1 = centerY - previousPoint1.Y; var squareDistanceP1 = dxP1 * dxP1 + dyP1 * dyP1; if (squareDistanceP1 > sqRadiusPlusTolerance) { //Make a new circle from the current two-point circle and the current point circle = new InternalCircle(circle.Point0, circle.Point1, previousPoint1); centerX = circle.CenterX; centerY = circle.CenterY; sqRadiusPlusTolerance = circle.SqRadius + sqTolerance; } else if (numPreviousPoints == 2) { var dxP2 = centerX - previousPoint2.X; var dyP2 = centerY - previousPoint2.Y; var squareDistanceP2 = dxP2 * dxP2 + dyP2 * dyP2; if (squareDistanceP2 > sqRadiusPlusTolerance) { //Make a new circle from the current two-point circle and the current point circle = new InternalCircle(circle.Point0, circle.Point1, previousPoint2); centerX = circle.CenterX; centerY = circle.CenterY; sqRadiusPlusTolerance = circle.SqRadius + sqTolerance; } } if (circle.SqRadius < priorRadius) { Debug.WriteLine("Bounding circle got smaller during this iteration"); } priorRadius = circle.SqRadius; stallCounter++; } if (stallCounter >= stallLimit) { Debug.WriteLine("Bounding circle failed to converge to within " + (Constants.BaseTolerance * circle.SqRadius * 2)); } #endregion #region Algorithm 3: Meggiddo's Linear-Time Algorithm //Pair up points into n/2 pairs. //If an odd number of points..... //Construct a bisecting line for each pair of points. This sets their slope. //Order the slopes. //Find the median (halfway in the set) slope of the bisector lines //Test #endregion var radius = circle.SqRadius.IsNegligible() ? 0 : Math.Sqrt(circle.SqRadius); return(new BoundingCircle(radius, new PointLight(centerX, centerY))); }
/// <summary> /// Finds the minimum bounding circle /// </summary> /// <param name="points">The points.</param> /// <returns>System.Double.</returns> /// <exception cref="Exception">Bounding circle failed to converge</exception> /// <references> /// Based on Emo Welzl's "move-to-front heuristic" and this paper (algorithm 1). /// http://www.inf.ethz.ch/personal/gaertner/texts/own_work/esa99_final.pdf /// This algorithm runs in near linear time. Visiting most points just a few times. /// Though a linear algorithm was found by Meggi do, this algorithm is more robust /// (doesn't care about multiple points on a line and fewer rounding functions) /// and directly applicable to multiple dimensions (in our case, just 2 and 3 D). /// </references> public static BoundingCircle MinimumCircle(IList <Point> points) { #region Algorithm 1 //Randomize the list of points //var r = new Random(); //var randomPoints = new List<Point>(points.OrderBy(p=>r.Next())); //if (randomPoints.Count < 2) return new BoundingCircle(0.0, points[0]); ////Get any two points in the list points. //var point1 = randomPoints[0]; //var point2 = randomPoints[1]; //var previousPoints = new List<Point>(); //var circle = new InternalCircle(new List<Point> {point1, point2}); //var stallCounter = 0; //var i = 0; //Algorithm 1 //while (i < randomPoints.Count && stallCounter < points.Count * 2) //{ // var currentPoint = randomPoints[i]; // //If the current point is part of the circle or inside the circle, go to the next iteration // if (circle.Points.Contains(currentPoint) || circle.IsPointInsideCircle(currentPoint)) // { // i++; // continue; // } // //Else if the currentPoint is a previousPoint, increase dimension // if (previousPoints.Contains(currentPoint)) // { // //Make a new circle from the current two-point circle and the current point // circle = new InternalCircle(new List<Point> {circle.Points[0], circle.Points[1], currentPoint}); // previousPoints.Remove(currentPoint); // i++; // } // else // { // //Find the point in the circle furthest from new point. // Point furthestPoint; // circle.Furthest(currentPoint, out furthestPoint, ref previousPoints); // //Make a new circle from the furthest point and current point // circle = new InternalCircle(new List<Point> {currentPoint, furthestPoint}); // //Add previousPoints to the front of the list // foreach (var previousPoint in previousPoints) // { // randomPoints.Remove(previousPoint); // randomPoints.Insert(0, previousPoint); // } // //Restart the search // stallCounter++; // i = 0; // } //} #endregion #region Algorithm 2: Furthest Point //Algorithm 2 var listPoints = new List <Point>(points); var point1 = listPoints.First(); var point2 = listPoints[listPoints.Count / 2]; var circle = new InternalCircle(new List <Point> { point1, point2 }); var stallCounter = 0; var previousPoints = new List <Point>(); var successful = false; var stallLimit = listPoints.Count * 3; if (stallLimit < 100) { stallLimit = 100; } while (!successful && stallCounter < stallLimit) { //Find the furthest point from the center point //If stallCounter is getting big, add a bit extra to the circle radius to ensure convergence if (stallCounter > stallLimit / 2) { circle.SqRadius = circle.SqRadius + Constants.BaseTolerance * circle.SqRadius; } //Add it a second time if stallCounter is even bigger if (stallCounter > stallLimit * 2 / 3) { circle.SqRadius = circle.SqRadius + Constants.BaseTolerance * circle.SqRadius; } var maxDistance = circle.SqRadius; Point nextPoint = null; var create3PointCircle = false; foreach (var point in listPoints) { if (previousPoints.Contains(point) && !circle.IsPointInsideCircle(point)) { create3PointCircle = true; nextPoint = point; break; } if (circle.Points.Contains(point)) { continue; //Check if point is already part of the circle } var squareDistanceToPoint = (circle.Center.X - point.X) * (circle.Center.X - point.X) + (circle.Center.Y - point.Y) * (circle.Center.Y - point.Y); if (squareDistanceToPoint <= maxDistance) { continue; //Beginning with the circle's square radius } maxDistance = squareDistanceToPoint; nextPoint = point; } if (nextPoint == null) { successful = true; continue; } //Create a new circle of either 2 or 3 points //if the currentPoint is a previousPoint, increase dimension if (create3PointCircle) { //Make a new circle from the current two-point circle and the current point circle = new InternalCircle(new List <Point> { circle.Points[0], circle.Points[1], nextPoint }); previousPoints.Remove(nextPoint); } else { //Find the point in the circle furthest from new point. Point furthestPoint; circle.Furthest(nextPoint, out furthestPoint, ref previousPoints); //Make a new circle from the furthest point and current point circle = new InternalCircle(new List <Point> { nextPoint, furthestPoint }); //Add previousPoints to the front of the list foreach (var previousPoint in previousPoints) { listPoints.Remove(previousPoint); listPoints.Insert(0, previousPoint); } } stallCounter++; } #endregion #region Algorithm 3: Meggiddo's Linear-Time Algorithm //Pair up points into n/2 pairs. //If an odd number of points..... //Construct a bisecting line for each pair of points. This sets their slope. //Order the slopes. //Find the median (halfway in the set) slope of the bisector lines //Test #endregion //Return information about minimum circle if (stallCounter >= stallLimit) { Debug.WriteLine("Bounding circle failed to converge to within " + (Constants.BaseTolerance * circle.SqRadius * 2)); } var radius = circle.SqRadius.IsNegligible() ? 0 : Math.Sqrt(circle.SqRadius); return(new BoundingCircle(radius, circle.Center)); }