我正在处理地理信息,最近我需要绘制一个椭圆。为了与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,那么椭圆顶部的点与椭圆左侧的点数相同。
这是浪费点的使用!在椭圆的上半部分添加一个点几乎不会影响多边形近似的精度,但在椭圆的左边部分添加一个点可能会产生重大影响。
我真正喜欢的是用多边形近似椭圆的更好算法。我需要这个算法:
我所想到的是找到一个多边形,其中每两条线之间的角度总是相同的 - 但不仅我无法找到如何制作这样的多边形,I&#39 ;即使我删除了限制,我甚至不确定是否存在!
有没有人知道如何找到这样的多边形?
答案 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
完美角度多边形的草图:
答案 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。