首页 > 解决方案 > 在给定角度的椭圆上找到点,然后反转该过程

问题描述

我的第一个任务很简单:找到要在屏幕上绘制的椭圆的点。我在下面创建了一个 Ellipse 类,其方法采用 0 到 2*PI 之间的角度并返回该点。

public class Ellipse
{
    public PointF Center { get; set; }
    public float A { get; set; }  /* horizontal semiaxis */
    public float B { get; set; }  /* vertical semiaxis */

    public Ellipse(PointF center, float a, float b)
    {
        this.Center = center;
        this.A = a;
        this.B = b;
    }        

    public PointF GetXYWhenT(float t_rad)
    {
        float x = this.Center.X + (this.A * (float)Math.Cos(t_rad));
        float y = this.Center.Y + (this.B * (float)Math.Sin(t_rad));
        return new PointF(x, y);
    }
}

我使用椭圆的参数方程,因为它对这项任务很方便。参数 t 是角度。计算 X 和 Y 值并将其放在椭圆上作为一个点。通过增加参数 t,我可以按照使绘制椭圆像连接点一样简单的顺序获得点。

private void RunTest1()
{
    PointF center = new PointF(0, 0);
    float a = 3;  /* horizontal semiaxis */
    float b = 4;  /* vertical semiaxis */
    Ellipse ellipse = new Ellipse(center, a, b);

    List<PointF> curve = new List<PointF>();   /* collects all points needed to draw the ellipse */

    float start = 0;
    float end = (float)(2 * Math.PI);  /* 360 degrees */
    float step = 0.0174533f;  /* 1 degree */

    for (float t_rad = start; t_rad <= end; t_rad += step)
    {
        PointF point = ellipse.GetXYWhenT(t_rad);
        curve.Add(point);
    }
}

RunTest X是我在 Main 中运行的方法。第一个会给我画这个椭圆所需的点。要点是正确的。 我已经使用我不会在此处包括的绘制方法直观地确认椭圆被绘制到规范。 绘制它不是问题。 这里的要点是,对于 t_rad 的每个值,我在曲线上都有一个对应点。

现在我需要在绘制椭圆后执行不同的任务。为此,我需要反转该过程,即在椭圆上取任意点并将其转换回 t_rad。Math.Atan2 应该可以解决问题。该方法称为GetTWhenPoint。它是 MyMath 类中的扩展方法。

public static class MyMath
{
    public static float GetTWhenPoint(this PointF center, PointF point)
    {
        float x = point.X - center.X;
        float y = point.Y - center.Y;

        float retval = (float)Math.Atan2(y, x);
        if (retval < 0)
        {
            retval += (float)(2 * Math.PI);
        }

        return retval;
    }
}

简单的三角函数,对吧?然而...

private void RunTest2()
{
    PointF center = new PointF(0, 0);
    float a = 3;  /* horizontal semiaxis */
    float b = 4;  /* vertical semiaxis */
    Ellipse ellipse = new Ellipse(center, a, b);

    string debug = "TEST 2\r\n";

    float start = 0;
    float end = (float)(2 * Math.PI);  
    float step = 0.0174533f;      

    for (float t_rad = start; t_rad <= end; t_rad += step)
    {
        PointF point = ellipse.GetXYWhenT(t_rad);
        double t_rad2 = center.GetTWhenPoint(point);
        debug += t_rad.ToString() + "\t" + t_rad2.ToString() + "\r\n";
    }

    Clipboard.SetText(debug);
}

当我使用它将点转换回 t_rad2 时,我希望它等于或非常接近原始 t_rad。

TEST 2
0   0
0.0174533   0.0232692267745733
0.0349066   0.0465274415910244
0.0523599   0.0697636753320694
0.0698132   0.0929670184850693
0.0872665   0.116126760840416
...
6.178444    6.14392471313477
6.195897    6.1670298576355
6.21335 6.19018936157227
6.230803    6.21339273452759
6.248257    6.23662853240967
6.26571 6.25988674163818
6.283163    6.28315591812134

我在这里想念什么?到目前为止,我所有的数字都是弧度(据我所知)。现在这里变得更奇怪了......

private void RunTest3()
{
    PointF center = new PointF(0, 0);
    float a = 4;  /* horizontal semiaxis */
    float b = 4;  /* vertical semiaxis */
    Ellipse ellipse = new Ellipse(center, a, b);

    string debug = "TEST 3\r\n";

    float start = 0;
    float end = (float)(2 * Math.PI);
    float step = 0.0174533f;  

    for (float t_rad = start; t_rad <= end; t_rad += step)
    {
        PointF point = ellipse.GetXYWhenT(t_rad);
        double t_rad2 = center.GetTWhenPoint(point);
        debug += t_rad.ToString() + "\t" + t_rad2.ToString() + "\r\n";
    }

    Clipboard.SetText(debug);    
}

如果我设置 a 和 b 等于使椭圆成为一个完美的圆,那么一切看起来都很正常!

TEST 3
0   0
0.0174533   0.0174532998353243
0.0349066   0.0349065996706486
0.0523599   0.0523599050939083
0.0698132   0.0698131918907166
0.0872665   0.0872664898633957
...    
6.178444    6.17844390869141
6.195897    6.19589710235596
6.21335 6.21335029602051
6.230803    6.23080348968506
6.248257    6.24825668334961
6.26571 6.26570987701416
6.283163    6.28316307067871

这告诉我的是,当我将点转换回 t_rad2 时,它会以某种方式受到椭圆尺寸的影响。但是怎么做?除了相对于笛卡尔原点 (0,0) 调整椭圆的中心外,GetTWhenPoint 方法不使用 Ellipse 类中的任何其他信息,特别是半轴。Math.Atan2 只需要该点的 x 和 y 值即可找到它与 0 度向量的夹角。这是基本的三角函数。

它甚至不应该关心它是椭圆上的一个点。从方法的上下文来看,它只是一个点,就像无限的任何其他点一样。我的扩展方法如何受到椭圆尺寸的影响?

是我的数学错了吗?我的意思是我已经有一段时间没有使用 trig 了,但我想我正确地记住了简单的那些。

提前致谢!

标签: c#

解决方案


我想这就是你想要的。

public class Ellipse
{
    public PointF Center { get; set; }
    public float A { get; set; }  /* horizontal semiaxis */
    public float B { get; set; }  /* vertical semiaxis */

    public Ellipse(PointF center, float a, float b)
    {
        this.Center=center;
        this.A=a;
        this.B=b;
    }

    public PointF GetXYWhenT(float t_rad)
    {
        float x = this.Center.X+(this.A*(float)Math.Cos(t_rad));
        float y = this.Center.Y+(this.B*(float)Math.Sin(t_rad));
        return new PointF(x, y);
    }

    public float GetParameterFromPoint(PointF point)
    {
        var x = point.X-Center.X;
        var y = point.Y-Center.Y;

        // Since x=a*cos(t) and y=b*sin(t), then
        // tan(t) = sin(t)/cos(t) = (y/b) / (x/a)
        return (float)Math.Atan2(A*y, B*x);
    }
}

class Program
{
    static readonly Random rng = new Random();
    static void Main(string[] args)
    {
        var center = new PointF(35.5f, -12.2f);
        var ellipse = new Ellipse(center, 18f, 44f);

        // Get t between -π and +π
        var t = (float)(2*Math.PI*rng.NextDouble()-Math.PI);
        var point = ellipse.GetXYWhenT(t);

        var t_check = ellipse.GetParameterFromPoint(point);

        Debug.WriteLine($"t={t}, t_check={t_check}");
        // t=-0.7434262, t_check=-0.7434263
    }
}

我会将曲线的正确参数化视为具有跨度在 0 和 1 之间的参数的曲线。因此,无需指定弧度

x = A*Cos(2*Math.PI*t)
y = B*Sin(2*Math.PI*t)

和相反

t = Atan2(A*y, B*x)/(2*PI)

另外,考虑一下椭圆在极坐标中相对于中心的样子。

x = A*Cos(t) = R*Cos(θ)     |  TAN(θ) = B/A*TAN(t)
y = B*Sin(t) = R*Sin(θ)     |  
                            |  R = Sqrt(B^2+(A^2-B^2)*Cos(t)^2)

                    A*B 
  R(θ) = ----------------------------
          Sqrt(A^2+(B^2-A^2)*Cos(θ)^2)

此外,请考虑以下辅助函数,它们围绕所需的象限(弧度版本)包裹角度

/// <summary>
/// Wraps angle between 0 and 2π
/// </summary>
/// <param name="angle">The angle</param>
/// <returns>A bounded angle value</returns>
public static double WrapTo2PI(this double angle) 
    => angle-(2*Math.PI)*Math.Floor(angle/(2*Math.PI));
/// <summary>
/// Wraps angle between -π and π
/// </summary>
/// <param name="angle">The angle</param>
/// <returns>A bounded angle value</returns>
public static double WrapBetweenPI(this double angle) 
    => angle+(2*Math.PI)*Math.Floor((Math.PI-angle)/(2*Math.PI));

和学位版本

/// <summary>
/// Wraps angle between 0 and 360
/// </summary>
/// <param name="angle">The angle</param>
/// <returns>A bounded angle value</returns>
public static double WrapTo360(this double angle) 
    => angle-360*Math.Floor(angle/360);
/// <summary>
/// Wraps angle between -180 and 180
/// </summary>
/// <param name="angle">The angle</param>
/// <returns>A bounded angle value</returns>
/// <remarks>see: http://stackoverflow.com/questions/7271527/inconsistency-with-math-round</remarks>
public static double WrapBetween180(this double angle)
    => angle+360*Math.Floor((180-angle)/360);

推荐阅读