如何计算两条直线之间的正角度和负角度?

13

这里有一个非常方便的二维几何工具集。

angleBetweenLines方法存在问题,它的结果总是正数。我需要检测正负角度,因此如果一条线比另一条线高或低15度,形状显然不同。

我的情况是一根线保持静止,而另一根线旋转,我需要通过将其与静止线进行比较来理解它的旋转方向。

编辑:作为对下面swestrup评论的回应,实际上我的情况是有一条单独的线,并记录其起始位置。然后该线从其起始位置旋转,我需要计算从其起始位置到当前位置的角度。例如,如果它顺时针旋转,则为正旋转;如果逆时针旋转,则为负旋转。(反之亦然)

如何改进算法,使其根据线的位置返回正负角度?


鉴于两个任意相交的线段,很难确定哪一个“在上面”,甚至很难确定要测量哪个角度,因为它们通常形成一个“X”形状。你是否总是使用具有共同起点的两条线?那样会简单得多。 - swestrup
抱歉,我澄清一下。我实际上是在谈论一条线及其相对于起始位置的旋转。 - Jaanus
你想要什么范围?你想要完整的-pi到pi,还是只需要-pi/2到pi/2,即你是否关心线的方向? - Troubadour
不用在意方向,我只需要监测旋转角度差大约45度左右。 - Jaanus
@Jaanus:在这种情况下,duffymo的答案已经足够了。 - Troubadour
@Troubadour:听起来很酷,但我希望能够懒一点,让答案用代码写出来 :P - Jaanus
6个回答

20
这是brainjam建议的实现方式。(它适用于我的限制条件,即行之间的差异保证足够小,无需进行任何归一化操作。)
CGFloat angleBetweenLinesInRad(CGPoint line1Start, CGPoint line1End, CGPoint line2Start, CGPoint line2End) {
    CGFloat a = line1End.x - line1Start.x;
    CGFloat b = line1End.y - line1Start.y;
    CGFloat c = line2End.x - line2Start.x;
    CGFloat d = line2End.y - line2Start.y;

    CGFloat atanA = atan2(a, b);
    CGFloat atanB = atan2(c, d);

    return atanA - atanB;
}

我喜欢它很简洁。矢量版本会更简洁吗?


1
我接受了brainjam的答案,因为我真的是在寻求算法方面的帮助,而且通常情况下,如果我的答案是另一个答案的衍生物,我不喜欢接受自己的答案,我更愿意给予他人应有的荣誉。 - Jaanus
+1 这太巧妙了!省去了我数小时的几何计算和调试时间! - K Mehta

9
这是一个涉及2D向量的简单问题。两个向量之间的夹角的正弦值与这两个向量的叉积有关。而“上方”或“下方”则由叉积产生的向量的符号决定:如果你对向量A和B进行叉乘,所得到的叉积为正,那么A在B“下方”;如果叉积为负,则A在B“上方”。详情请参见Mathworld
以下是我如何在Java中编写它的代码示例:
package cruft;

import java.text.DecimalFormat;
import java.text.NumberFormat;

/**
 * VectorUtils
 * User: Michael
 * Date: Apr 18, 2010
 * Time: 4:12:45 PM
 */
public class VectorUtils
{
    private static final int DEFAULT_DIMENSIONS = 3;
    private static final NumberFormat DEFAULT_FORMAT = new DecimalFormat("0.###");

    public static void main(String[] args)
    {
        double [] a = { 1.0, 0.0, 0.0 };
        double [] b = { 0.0, 1.0, 0.0 };

        double [] c = VectorUtils.crossProduct(a, b);

        System.out.println(VectorUtils.toString(c));
    }

    public static double [] crossProduct(double [] a, double [] b)
    {
        assert ((a != null) && (a.length >= DEFAULT_DIMENSIONS ) && (b != null) && (b.length >= DEFAULT_DIMENSIONS));

        double [] c = new double[DEFAULT_DIMENSIONS];

        c[0] = +a[1]*b[2] - a[2]*b[1];
        c[1] = +a[2]*b[0] - a[0]*b[2];
        c[2] = +a[0]*b[1] - a[1]*b[0];

        return c;
    }

    public static String toString(double [] a)
    {
        StringBuilder builder = new StringBuilder(128);

        builder.append("{ ");

        for (double c : a)
        {
            builder.append(DEFAULT_FORMAT.format(c)).append(' ');
        }

        builder.append("}");

        return builder.toString();
    }
}

检查第三个分量的符号。如果是正数,则A在B“下方”;如果是负数,则A在B“上方”,只要这两个向量在y轴右侧的两个象限中。显然,如果它们都在y轴左侧的两个象限中,则相反是真的。
您需要考虑“上方”和“下方”的直觉概念。如果A在第一象限(0 <= θ <= 90)中,而B在第二象限(90 <= θ <= 180)中怎么办? “上方”和“下方”失去了意义。
线条随后从其起始位置旋转,我需要计算从其起始位置到当前位置的角度。例如,如果它顺时针旋转,则为正旋转;如果逆时针旋转,则为负旋转。(或反之亦然。)
这正是叉积的用途。当您从旋转平面向下看时,第三个分量的符号为逆时针为正,顺时针为负。

这听起来很不错,但将向量乘法转换为代码有点超出我的能力范围,我希望能够懒一点并以代码形式获得它 :P 即输入:我拥有的,两条线段端点的坐标;输出:有符号角度。 - Jaanus
来嘛,向量乘法只是乘法。你能编写一个配方吗?输入两个向量,输出一个向量。你想用哪种语言实现它? - duffymo
哇,感谢您的整理。我发现我的atan2版本更适合我的需求,但也许对其他阅读此内容的人会有帮助。 - Jaanus

9
@duffymo的回答是正确的,但如果您不想实现叉积,可以使用atan2函数。它返回-π到π之间的角度,并且您可以在每条线(或更准确地说是表示线的向量)上使用它。
如果您为第一条(静止的)线获得了一个角度θ,则必须将第二条线的角度φ归一化为θ-π和θ+π之间(通过添加±2π)。然后两条线之间的夹角将为φ-θ。

2
所有这些向量的东西都让我头炸了。(抱歉,我忘记了很多数学知识...)现在,我只是计算两条线的atan2并进行比较,效果很好。谢谢。 - Jaanus

1
一种“快速且不太规范”的方法是引入第三个参考线R。因此,给定两条线A和B,计算A和R之间的角度,然后计算B和R之间的角度,并相减。
这种方法实际上需要进行大约两倍的计算量,但易于解释和调试。

我理解atan2基本上就是这样做的,其中x轴是参考线。 - Jaanus

1
// Considering two vectors CA and BA
// Computing angle from CA to BA
// Thanks to code shared by Jaanus, but atan2(y,x) is used wrongly.

float getAngleBetweenVectorsWithSignInDeg(Point2f C, Point2f A, Point2f B)
{      
    float a = A.x - C.x;
    float b = A.y - C.y;
    float c = B.x - C.x;
    float d = B.y - C.y;

    float angleA = atan2(b, a);
    float angleB = atan2(d, c);
    cout << "angleA: " << angleA << "rad, " << angleA * 180 / M_PI << " deg" << endl;
    cout << "angleB: " << angleB << "rad, " << angleB * 180 / M_PI << " deg" << endl;
    float rotationAngleRad = angleB - angleA;
    float thetaDeg = rotationAngleRad * 180.0f / M_PI;
    return thetaDeg;
}

0

那个函数在RADS中运行正常

一个完整的圆有2pi RADS(360度)

因此,我相信你要找的答案就是返回值-2pi

如果你想让这个函数同时返回两个值,那么你就是在要求打破语言规则,一个函数只能返回一个值。你可以传递两个指针给它,让它用来设置值,这样改变就可以在函数结束后持续存在,你的程序也可以继续工作。但这并不是解决这个问题的明智方式。

编辑

刚刚注意到这个函数实际上在返回值时将Rads转换为Degrees。但是同样的原理仍然适用。


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