用多边形逼近椭圆

时间:2014-03-27 17:23:42

标签: c# geometry polygon computational-geometry ellipse

我正在处理地理信息,最近我需要绘制一个椭圆。为了与OGC约定兼容,我不能原样使用椭圆;相反,我使用多边形来近似椭圆,通过采用椭圆包含的多边形并使用任意多个点。

我用于为给定数量的点N生成椭圆的过程如下(使用C#和虚构的Polygon类):

Polygon CreateEllipsePolygon(Coordinate center, double radiusX, double radiusY, int numberOfPoints)
{
    Polygon result = new Polygon();
    for (int i=0;i<numberOfPoints;i++)
    {
        double percentDone = ((double)i)/((double)numberOfPoints);
        double currentEllipseAngle = percentDone * 2 * Math.PI;
        Point newPoint = CalculatePointOnEllipseForAngle(currentEllipseAngle, center, radiusX, radiusY);
        result.Add(newPoint);
    }
    return result;
}

到目前为止,这对我来说很有用,但是我注意到它有一个问题:如果我的椭圆是“矮胖的”,那就是,radiusX远大于radiusY,那么椭圆顶部的点与椭圆左侧的点数相同。

Illustration

这是浪费点的使用!在椭圆的上半部分添加一个点几乎不会影响多边形近似的精度,但在椭圆的左边部分添加一个点可能会产生重大影响。

我真正喜欢的是用多边形近似椭圆的更好算法。我需要这个算法:

  • 必须接受点数作为参数;可以接受每个象限中的点数(我可以迭代地在有问题的地方添加点数,但我需要很好地控制我和我的点数#39; m using)
  • 必须以椭圆为界
  • 它必须包含正上方,正下方,直线左侧和椭圆形中心右侧的点
  • 它的面积应该尽可能接近椭圆的面积,当然优先选择给定的点数(参见Jaan的回答 - 看来这个解决方案是已经最优)
  • 多边形中的最小内角是最大的

我所想到的是找到一个多边形,其中每两条线之间的角度总是相同的 - 但不仅我无法找到如何制作这样的多边形,I&#39 ;即使我删除了限制,我甚至不确定是否存在!

有没有人知道如何找到这样的多边形?

5 个答案:

答案 0 :(得分:9)

finding a polygon in which the angle between every two lines is
always the same

是的,有可能。我们想要找到(第一个)椭圆象限的这些点,这些点中的切线角度形成等距(相同的角度差)序列。不难发现切点

x=a*Cos(fi)
y=b*Sin(Fi)

derivatives
dx=-a*Sin(Fi), dy=b*Cos(Fi)
y'=dy/dx=-b/a*Cos(Fi)/Sin(Fi)=-b/a*Ctg(Fi) 

导数y'描述切线,此切线具有角系数

k=b/a*Cotangent(Fi)=Tg(Theta)
Fi = ArcCotangent(a/b*Tg(Theta)) = Pi/2-ArcTan(a/b*Tg(Theta)) 

归因于relation for complementary angles

其中Fi从0变化到Pi / 2,并且Theta - 从Pi / 2变为0 因此,每个象限找到N + 1个点(包括极值点)的代码可能看起来像(这是Delphi代码生成附图)

  for i := 0 to N - 1 do begin
    Theta := Pi/2 * i /  N;
    Fi :=  Pi/2 - ArcTan(Tan(Theta) * a/b);
    x := CenterX + Round(a * Cos(Fi));
    y := CenterY + Round(b * Sin(Fi));
  end;
  // I've removed Nth point calculation, that involves indefinite Tan(Pi/2) 
  // It would better to assign known value 0 to Fi in this point

enter image description here

完美角度多边形的草图:

enter image description here

答案 1 :(得分:3)

实现封闭轮廓(如椭圆)的自适应离散化的一种方法是反向运行Ramer–Douglas–Peucker algorithm

1. Start with a coarse description of the contour C, in this case 4 
   points located at the left, right, top and bottom of the ellipse.
2. Push the initial 4 edges onto a queue Q.

while (N < Nmax && Q not empty)

3. Pop an edge [pi,pj] <- Q, where pi,pj are the endpoints.
4. Project a midpoint pk onto the contour C. (I expect that 
   simply bisecting the theta endpoint values will suffice
   for an ellipse).
5. Calculate distance D between point pk and edge [pi,pj].

    if (D > TOL)

6.      Replace edge [pi,pj] with sub-edges [pi,pk], [pk,pj].
7.      Push new edges onto Q.
8.      N = N+1

    endif

endwhile

该算法迭代地改进了轮廓C的初始离散化,在高曲率区域中聚类点。如果(i)满足用户定义的容错TOL,或(ii)使用了最大允许点数Nmax,则会终止。

我确信有可能找到一个专门针对椭圆情况进行优化的替代方案,但我认为这种方法的一般性非常方便。

答案 2 :(得分:3)

我假设在OP的问题中,CalculatePointOnEllipseForAngle返回一个坐标如下的点。

newPoint.x = radiusX*cos(currentEllipseAngle) + center.x
newPoint.y = radiusY*sin(currentEllipseAngle) + center.y

然后,如果目标是最小化椭圆和内接多边形的区域的差异(即,找到具有最大区域的内接多边形),OP的原始解已经是最佳的。见Ivan Niven, "Maxima and Minima Without Calculus", Theorem 7.3b。 (有无限多个最优解:通过在上面的公式中向currentEllipseAngle添加任意常数,可以得到具有相同面积的另一个多边形;这些是唯一的最优解。证明理念很简单:第一个证明这些是圆的情况下的最优解,即radiusX = radiusY;其次,观察到在将圆转换为椭圆的线性变换下,例如乘以x的变换通过某个常数坐标,所有区域都乘以一个常数,因此圆的最大区域内接多边形将转换为椭圆的最大区域内接多边形。)

其他人可能也会考虑其他目标,例如:最大化多边形的最小角度或最小化多边形和椭圆的边界之间的Hausdorff distance。 (例如Ramer-Douglas-Peucker algorithm是一种近似解决后一问题的启发式方法。与通常的Ramer-Douglas-Peucker实现中的近似多边形曲线不同,我们近似椭圆,但可以设计一个公式在椭圆弧上找到距线段最远的点。)关于这些目标,OP的解决方案通常不是最优的,我不知道找到一个精确的解公式是否可行。但是OP的解决方案没有OP的图片那么糟糕:看起来OP的图像还没有使用这种算法生成,因为它在椭圆的更尖锐的弯曲部分中的点比该算法产生的点少。

答案 3 :(得分:1)

这是我使用的迭代算法。

我没有寻找理论上最优的解决方案,但它对我来说效果很好。

请注意,此算法将椭圆的多边形素数的最大误差作为输入,而不是您希望的点数。

public static class EllipsePolygonCreator
{
#region Public static methods

public static IEnumerable<Coordinate> CreateEllipsePoints(
  double maxAngleErrorRadians,
  double width,
  double height)
{
  IEnumerable<double> thetas = CreateEllipseThetas(maxAngleErrorRadians, width, height);
  return thetas.Select(theta => GetPointOnEllipse(theta, width, height));
}

#endregion

#region Private methods

private static IEnumerable<double> CreateEllipseThetas(
  double maxAngleErrorRadians,
  double width,
  double height)
{
  double firstQuarterStart = 0;
  double firstQuarterEnd = Math.PI / 2;
  double startPrimeAngle = Math.PI / 2;
  double endPrimeAngle = 0;

  double[] thetasFirstQuarter = RecursiveCreateEllipsePoints(
    firstQuarterStart,
    firstQuarterEnd,
    maxAngleErrorRadians,
    width / height,
    startPrimeAngle,
    endPrimeAngle).ToArray();

  double[] thetasSecondQuarter = new double[thetasFirstQuarter.Length];
  for (int i = 0; i < thetasFirstQuarter.Length; ++i)
  {
    thetasSecondQuarter[i] = Math.PI - thetasFirstQuarter[thetasFirstQuarter.Length - i - 1];
  }

  IEnumerable<double> thetasFirstHalf = thetasFirstQuarter.Concat(thetasSecondQuarter);
  IEnumerable<double> thetasSecondHalf = thetasFirstHalf.Select(theta => theta + Math.PI);
  IEnumerable<double> thetas = thetasFirstHalf.Concat(thetasSecondHalf);
  return thetas;
}

private static IEnumerable<double> RecursiveCreateEllipsePoints(
  double startTheta,
  double endTheta,
  double maxAngleError,
  double widthHeightRatio,
  double startPrimeAngle,
  double endPrimeAngle)
{
  double yDelta = Math.Sin(endTheta) - Math.Sin(startTheta);
  double xDelta = Math.Cos(startTheta) - Math.Cos(endTheta);
  double averageAngle = Math.Atan2(yDelta, xDelta * widthHeightRatio);

  if (Math.Abs(averageAngle - startPrimeAngle) < maxAngleError &&
      Math.Abs(averageAngle - endPrimeAngle) < maxAngleError)
  {
    return new double[] { endTheta };
  }

  double middleTheta = (startTheta + endTheta) / 2;
  double middlePrimeAngle = GetPrimeAngle(middleTheta, widthHeightRatio);
  IEnumerable<double> firstPoints = RecursiveCreateEllipsePoints(
    startTheta,
    middleTheta,
    maxAngleError,
    widthHeightRatio,
    startPrimeAngle,
    middlePrimeAngle);
  IEnumerable<double> lastPoints = RecursiveCreateEllipsePoints(
    middleTheta,
    endTheta,
    maxAngleError,
    widthHeightRatio,
    middlePrimeAngle,
    endPrimeAngle);

  return firstPoints.Concat(lastPoints);
}

private static double GetPrimeAngle(double theta, double widthHeightRatio)
{
  return Math.Atan(1 / (Math.Tan(theta) * widthHeightRatio)); // Prime of an ellipse
}

private static Coordinate GetPointOnEllipse(double theta, double width, double height)
{
  double x = width * Math.Cos(theta);
  double y = height * Math.Sin(theta);
  return new Coordinate(x, y);
}

#endregion
}

答案 4 :(得分:0)

我建议你切换到极坐标:

极坐标中的椭圆是:

x(t) = XRadius * cos(t)
y(t) = YRadius * sin(t)

代表0 <= t <= 2*pi

当Xradius&gt;&gt;时出现问题YRadius(或Yradius&gt;&gt; Yradius)

您可以使用明显不完全相同的角度数组,而不是使用numberOfPoints。 即如果得分为36分,则每个部门得到angle = 2*pi*n / 36 radiants。 当你在这两个值的“邻域”中绕n = 0(或36)或n = 18时,近似方法不能很好地工作,因为椭圆扇区与用于近似它的三角形明显不同。您可以减小此点周围的扇区大小,从而提高精度。而不仅仅是增加点数,这也会增加其他不需要的区域中的细分。角度序列应该像(以度为单位):

angles_array = [5,10,10,10,10.....,5,5,....10,10,...5]

前5度对于t = 0,序列是t = 0,对于t = pi,t = 0,并且最后一个是2 * pi。