【问题标题】:Angle between 3 points?3点之间的角度?
【发布时间】:2010-08-15 04:18:21
【问题描述】:

给定点ABC,我怎么能找到角ABC?我正在为矢量绘图应用程序制作一个feehand工具,并尽量减少它生成的点数,除非鼠标位置和最后两个点的角度大于某个阈值,否则我不会添加点。 谢谢

我有什么:

int CGlEngineFunctions::GetAngleABC( POINTFLOAT a, POINTFLOAT b, POINTFLOAT c )
{
    POINTFLOAT ab;
    POINTFLOAT ac;

    ab.x = b.x - a.x;
    ab.y = b.y - a.y;

    ac.x = b.x - c.x;
    ac.y = b.y - c.y;

    float dotabac = (ab.x * ab.y + ac.x * ac.y);
    float lenab = sqrt(ab.x * ab.x + ab.y * ab.y);
    float lenac = sqrt(ac.x * ac.x + ac.y * ac.y);

    float dacos = dotabac / lenab / lenac;

    float rslt = acos(dacos);
    float rs = (rslt * 180) / 3.141592;
     RoundNumber(rs);
     return (int)rs;


}

【问题讨论】:

  • 我做得很好,我确实有一个算法,但它没有起到作用。
  • @abelenky:这使得问题“不清楚或无用”究竟如何?您可能误解了代表的目的。它不是为了让你惩罚那些试图做对他们来说是新事物的人。

标签: c++ c algorithm trigonometry


【解决方案1】:

关于您的方法的初步建议:

你所谓的ac 实际上是cb。不过没关系,这才是真正需要的。 接下来,

float dotabac = (ab.x * ab.y + ac.x * ac.y);

这是你的第一个错误。两个向量的实数点积是:

float dotabac = (ab.x * ac.x + ab.y * ac.y);

现在,

float rslt = acos(dacos);

在这里您应该注意,由于计算过程中的一些精度损失,理论上dacos 可能会变得大于 1(或小于 -1)。因此 - 你应该明确地检查这一点。

加上性能说明:您调用了两次重的sqrt 函数来计算两个向量的长度。然后将点积除以这些长度。 相反,您可以在两个向量的长度平方相乘时调用 sqrt

最后,您应该注意,您的结果精确到sign。也就是说,您的方法不会区分 20° 和 -20°,因为两者的余弦相同。 您的方法将为 ABC 和 CBA 产生相同的角度。

计算角度的一种正确方法是“oslvbo”建议:

float angba = atan2(ab.y, ab.x);
float angbc = atan2(cb.y, cb.x);
float rslt = angba - angbc;
float rs = (rslt * 180) / 3.141592;

(我刚刚将atan 替换为atan2)。

这是最简单的方法,总能得出正确的结果。这种方法的缺点是你实际上调用了一个重三角函数atan2 两次。

我建议以下方法。它有点复杂(需要一些三角函数才能理解),但是从性能的角度来看它是优越的。 它只调用一次三角函数atan2。并且没有平方根计算。

int CGlEngineFunctions::GetAngleABC( POINTFLOAT a, POINTFLOAT b, POINTFLOAT c )
{
    POINTFLOAT ab = { b.x - a.x, b.y - a.y };
    POINTFLOAT cb = { b.x - c.x, b.y - c.y };

    // dot product  
    float dot = (ab.x * cb.x + ab.y * cb.y);

    // length square of both vectors
    float abSqr = ab.x * ab.x + ab.y * ab.y;
    float cbSqr = cb.x * cb.x + cb.y * cb.y;

    // square of cosine of the needed angle    
    float cosSqr = dot * dot / abSqr / cbSqr;

    // this is a known trigonometric equality:
    // cos(alpha * 2) = [ cos(alpha) ]^2 * 2 - 1
    float cos2 = 2 * cosSqr - 1;

    // Here's the only invocation of the heavy function.
    // It's a good idea to check explicitly if cos2 is within [-1 .. 1] range

    const float pi = 3.141592f;

    float alpha2 =
        (cos2 <= -1) ? pi :
        (cos2 >= 1) ? 0 :
        acosf(cos2);

    float rslt = alpha2 / 2;

    float rs = rslt * 180. / pi;


    // Now revolve the ambiguities.
    // 1. If dot product of two vectors is negative - the angle is definitely
    // above 90 degrees. Still we have no information regarding the sign of the angle.

    // NOTE: This ambiguity is the consequence of our method: calculating the cosine
    // of the double angle. This allows us to get rid of calling sqrt.

    if (dot < 0)
        rs = 180 - rs;

    // 2. Determine the sign. For this we'll use the Determinant of two vectors.

    float det = (ab.x * cb.y - ab.y * cb.y);
    if (det < 0)
        rs = -rs;

    return (int) floor(rs + 0.5);


}

编辑:

最近我一直在研究一个相关的主题。然后我意识到有更好的方法。它实际上或多或少是相同的(在幕后)。然而,恕我直言,它更简单。

这个想法是旋转两个向量,使第一个向量与(正)X 方向对齐。显然旋转两个向量不会影响它们之间的角度。 OTOH 在这样的旋转之后,只需找出第二个向量相对于 X 轴的角度。这正是atan2 的用途。

旋转是通过将向量乘以以下矩阵来实现的:

  • a.x, a.y
  • -a.y, a.x

一旦看到向量a乘以这样一个矩阵,确实会朝X轴正方向旋转。

注意:严格来说,上面的矩阵不仅仅是旋转,它也是缩放。但这在我们的例子中是可以的,因为唯一重要的是矢量方向,而不是它的长度。

旋转矢量b 变为:

  • a.x * b.x + a.y * b.y = ab
  • -a.y * b.x + a.x * b.y = a 交叉 b

最后,答案可以表示为:

int CGlEngineFunctions::GetAngleABC( POINTFLOAT a, POINTFLOAT b, POINTFLOAT c )
{
    POINTFLOAT ab = { b.x - a.x, b.y - a.y };
    POINTFLOAT cb = { b.x - c.x, b.y - c.y };

    float dot = (ab.x * cb.x + ab.y * cb.y); // dot product
    float cross = (ab.x * cb.y - ab.y * cb.x); // cross product

    float alpha = atan2(cross, dot);

    return (int) floor(alpha * 180. / pi + 0.5);
}

【讨论】:

  • 不错的解决方案!在阅读您的答案之前,我很担心如何处理符号角度问题
  • 最后一个函数(return (int) floor(alpha * 180. / pi + 0.5);)很好,对 abc 和 cba 给出了不同的答案。效果很好!
  • 唯一没有给我带来精度问题的解决方案。谢谢!
  • angba = atan2(ab.y, ab.x); angbc = atan2(cb.y, cb.x); rslt = angba - angbc; rs = (rslt * 180) / pi; 返回[-360 ... +360] 范围内的答案 --> 两圈。
  • 1) 后面的GetAngleABC() 使用float 变量和double 常量的混合。只推荐一种类型。 2)不要使用+0.5技巧,这对于负alpha和其他一些值是错误的,使用lrint(alpha * (float)(180.0 / pi) )
【解决方案2】:

β = arccos((a^2 + c^2 - b^2) / 2ac)

其中a是对角α,b是对角β,c是对角γ。所以β就是你所说的角ABC。

【讨论】:

  • @Milo:你不知道——他使用abc 作为点对之间的距离。
  • β的符号呢?此方法不会区分 abc 和 cba。如果这是意图 - 没关系,但它是吗?
  • @valdo,我不打算保留符号,我不认为 OP 想要(他的角度阈值似乎基于幅度)。不过,你说得对,他应该意识到这一点。
【解决方案3】:

使用arccos 的方法很危险,因为我们冒着让它的参数等于1.0000001 并最终导致EDOMAIN 错误的风险。即使atan 的方法也是危险的,因为它涉及到除法,可能导致除法为零错误。最好使用atan2,将dxdy 值传递给它。

【讨论】:

    【解决方案4】:

    这是一种快速正确的计算直角值的方法:

    double AngleBetweenThreePoints(POINTFLOAT pointA, POINTFLOAT pointB, POINTFLOAT pointC)
    {
        float a = pointB.x - pointA.x;
        float b = pointB.y - pointA.y;
        float c = pointB.x - pointC.x;
        float d = pointB.y - pointC.y;
    
        float atanA = atan2(a, b);
        float atanB = atan2(c, d);
    
        return atanB - atanA;
    } 
    

    【讨论】:

    • 这会返回[-2*pi ... +2*pi] 范围内的答案(2 圈)。
    【解决方案5】:

    离题?但是你可以用余弦定律来做到这一点:

    求 A 和 B 之间的距离(称为 x)、B 和 C 之间的距离(称为 y)以及 A 和 C 之间的距离(称为 z)。

    那你知道 z^2=x^2+y^2-2*xycos(你想要的角度)

    因此,该角度为 cos^-1((z^2-x^2-y^2)/(2xy))=ANGLE

    【讨论】:

    • 你丢失了分数中的负数。应该是x^2 + y^2 - z^2,就像我的回答一样。
    【解决方案6】:
    float angba = atan((a.y - b.y) / (a.x - b.x));
    float angbc = atan((c.y - b.y) / (c.x - b.y));
    float rslt = angba - angbc;
    float rs = (rslt * 180) / 3.141592;
    

    【讨论】:

    • 与其使用 atan(dy/dx) 不如使用 atan2(dy, dx)
    【解决方案7】:

    这是一种以 B 为顶点获取 3 个点(A、B、C)之间角度的 OpenCV 方法:

    int getAngleABC( cv::Point2d a, cv::Point2d b, cv::Point2d c )
    {
        cv::Point2d ab = { b.x - a.x, b.y - a.y };
        cv::Point2d cb = { b.x - c.x, b.y - c.y };
    
        float dot = (ab.x * cb.x + ab.y * cb.y); // dot product
        float cross = (ab.x * cb.y - ab.y * cb.x); // cross product
    
        float alpha = atan2(cross, dot);
    
        return (int) floor(alpha * 180. / M_PI + 0.5);
    }
    

    基于@valdo 的优秀解决方案

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-09-04
      • 2016-09-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多