三点间的角度是多少?

20

给定三个点ABC,如何找到角ABC?我正在制作一个用于矢量绘图应用程序的手绘工具,并且为了最小化生成的点数,除非鼠标位置和上次添加的两个点之间的角度大于某个阈值,否则不会添加点。

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;


}

2
我的情况还不错,我确实有一个算法,但它似乎没有效果。 - jmasterx
5
那么这个问题为什么被认为是“不清楚或无用”的呢?你可能误解了声望的作用。它的目的并不是让你惩罚那些尝试做一些新事情的人。请问需要翻译成中文吗? - jalf
7个回答

38

关于你的方法,以下是建议:

你所称的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 函数。

最后,需要注意的是你的结果仅在符号上准确。也就是说,你的方法无法区分 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;

(我刚刚用 atan2 替换了 atan。)

这是最简单的方法,总是得到正确的结果。这种方法的缺点是你实际上要调用一个复杂的三角函数 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方向对齐。显然,旋转两个向量不会影响它们之间的角度。另一方面,在这样的旋转之后,只需找出第二个向量相对于X轴的角度即可。这正是 atan2 的作用。

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

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

可以看到,向量 a 乘以这样的矩阵确实朝着正的 X 轴旋转。

注意: 严格来说,上述矩阵不仅仅是旋转,它也进行了缩放。但在我们的情况下这没关系,因为唯一重要的是向量方向,而不是其长度。

旋转后的向量 b 变为:

  • a.x * b.x + a.y * b.y = a 点乘 b
  • -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);
}

不错的解决方案!在阅读了你的答案之前,我一直担心如何处理符号角度问题。 - Anh Tuan
最后一个函数(其中有 return (int) floor(alpha * 180. / pi + 0.5);)很好,对于 abc 和 cba 给出了不同的答案。运行良好! - Timo Kähkönen
唯一没有给我精度问题的解决方案。谢谢! - Bitcoin Cash - ADA enthusiast
1
angba = atan2(ab.y, ab.x); angbc = atan2(cb.y, cb.x); rslt = angba - angbc; rs = (rslt * 180) / pi; 返回的答案在[-360 ... +360]范围内 --> 两个循环。 - chux - Reinstate Monica
2
  1. 后面的 GetAngleABC() 函数使用了 float 变量和 double 常量的混合。建议只使用一种类型。
  2. 不要使用 +0.5 的技巧,因为它对于负数 alpha 和其他一些值是错误的,而是使用 lrint(alpha * (float)(180.0 / pi))
- chux - Reinstate Monica
显示剩余2条评论

5

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

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;
} 

1
这将返回[-2*pi ... +2*pi]范围内的答案(2个周期)。 - chux - Reinstate Monica

4

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

其中,a是α角对面的边长,b是β角对面的边长,c是γ角对面的边长。因此,β就是你所说的ABC角。


@MiloпјҡдҪ дёҚз”ЁжӢ…еҝғвҖ”вҖ”д»–жҳҜжҠҠaгҖҒbе’ҢcдҪңдёәзӮ№еҜ№д№Ӣй—ҙзҡ„и·қзҰ»гҖӮ - Jerry Coffin
β的符号怎么样? 这种方法无法区分abc和cba。如果这是意图-那没问题,但是真的吗? - valdo
@valdo,我并不打算保留符号,而且我认为 OP 也不想要(他的角度阈值似乎是基于大小)。然而,你说得对,他应该意识到这一点。 - Matthew Flaschen

4

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


2

这里是使用OpenCV获取三点(A、B、C)之间角度的方法,其中以B为顶点:

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 的出色解决方案


1
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;

2
使用atan2(dy, dx)比使用atan(dy/dx)更好。 - valdo

1

离题了?但是你可以用余弦定理来解决:

找到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))=角度


你在分数中丢失了负号。应该像我的答案一样是 x^2 + y^2 - z^2 - Matthew Flaschen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接