如何找到围绕一个点的两个角度之间的最小差异?

196

给定一个2D圆形,其围绕坐标轴有两个角度,范围为-PI -> PI,求它们之间的最小角度值是多少?

需要考虑到PI和-PI之间的差值不是2PI而是零。

举个例子:

想象一个圆,中心有两条线,它们之间有2个角度,内部的角度被称为较小的角度,外部的角度被称为较大的角度。

两个角度加起来总共是一个完整的圆。鉴于每个角度都可以适合某个特定范围内,考虑到翻转,较小的角度值是多少


3
我读了3遍才理解你的意思。请加入一个例子,或者更好地解释一下... - Kobi
想象一个圆,从中心点出发有两条线,这两条线之间有两个角度,它们在内部形成的角度称为小角度,而在外部形成的角度则称为大角度。当两个角度相加时,它们组成了一个完整的圆。鉴于每个角度都可以适应一定的范围,考虑到回卷,那么小角度的值是多少? - Tom J Nowell
可能是重复的问题:如何计算一条线与水平轴之间的角度? - Jim G.
2
@JimG。这不是同一个问题,本问题中使用的角度P1将是另一个较小的角度,而不是其他问题中的错误答案。此外,并没有保证该角度与水平轴相同。 - Tom J Nowell
1
如果您使用Unity c#脚本,可以使用Mathf.DeltaAngle函数。 - bigant02
@bigant02,非常感谢。我在Unity中正在研究这个确切的问题。 :) 你帮了我很多 :) - JAQuent
11个回答

273

这个可以给出任意角度的有符号角:

a = targetA - sourceA
a = (a + 180) % 360 - 180

请注意,在许多编程语言中,取模运算返回与被除数相同符号的值(例如C、C++、C#、JavaScript等, 完整列表在此处)。这需要使用自定义mod函数来实现:

mod = (a, n) -> a - floor(a/n) * n

或者如此:

mod = (a, n) -> (a % n + n) % n
如果角度在[-180, 180]范围内,这也可以工作:
a = targetA - sourceA
a += (a>180) ? -360 : (a<-180) ? 360 : 0

更详细的说:

a = targetA - sourceA
a -= 360 if a > 180
a += 360 if a < -180

4
虽然有人可能想要执行取余运算 % 360,例如如果我有角度0和目标角度721,正确的答案应该是1,而上述方法给出的答案却是361。 - Tom J Nowell
1
一种更简洁但可能更昂贵的方法是使用以下语句替代后一种方法的第二个语句:a -= 360*sgn(a)*(abs(a) > 180)。(想一想,如果您有无分支的 sgnabs 实现,那么这个特性实际上可能开始弥补需要两个乘法的缺点。) - mmirate
2
“任意角度的有符号角度”示例在大多数情况下都有效,但有一个例外。 在场景double targetA = 2; double sourceA = 359;中,“a”的值将等于-357.0而不是3.0。 - Stevoisiak
3
在C++中,你可以使用std::fmod(a,360)或fmod(a,360)来使用浮点数取模。 - Joeppie
2
如果在您的语言中,% 运算符像余数一样(保留符号),那么您可以简单地添加额外的 360 而不是定义一个模数函数: a = (a + 540) % 360 - 180 如上所述,这仅适用于相差不超过 360 度的角度,这种情况可能经常出现。否则: a = ((a % 360) + 540) % 360 - 180 - RedMatt
显示剩余6条评论

184

x是目标角度,y是源角度或起始角度:

atan2(sin(x-y), cos(x-y))

它返回带符号的角度差。请注意,根据您的API,atan2()函数的参数顺序可能会有所不同。


18
x-y 给出的是角度差,但它可能超出了所需范围。将这个角度视为单位圆上的一个点,该点的坐标为 (cos(x-y), sin(x-y))atan2 返回该点的角度(等同于 x-y),但其范围为 [-PI, PI]。 - Max
4
这通过了测试套件 https://gist.github.com/bradphelan/7fe21ad8ebfcb43696b8 - bradgonesurfing
5
一条简单的解决方案,解决了我的问题(不是所选答案 ;) )。但是反正切是一个耗费时间的过程。 - Mohan Kumar
3
对我而言,这是最优雅的解决方案。可惜它可能会在计算上显得比较昂贵。 - focs
2
不幸的是,这个解决方案也不像其他解决方案那样精确。 - 12Me21
显示剩余3条评论

59

如果你的两个角度是x和y,那么它们之间的一个角度为abs(x-y)。另一个角度为(2*PI)- abs(x-y)。因此,其中较小的角度的值为:

min((2 * PI) - abs(x - y), abs(x - y))

这将为您提供角度的绝对值,并假设输入已经归一化(即在范围[0, 2π)内)。

如果您想保留角度的符号(即方向),并且还接受范围[0, 2π)之外的角度,您可以进行概括。以下是通用版本的Python代码:

PI = math.pi
TAU = 2*PI
def smallestSignedAngleBetween(x, y):
    a = (x - y) % TAU
    b = (y - x) % TAU
    return -a if a < b else b

请注意,在涉及负值时,% 运算符在不同的语言中的行为可能不同,因此如果需要移植代码,则可能需要进行一些符号调整。


1
@bradgonesurfing 这是/曾经是正确的,但公平地说,您的测试检查了原始问题中未指定的内容,特别是非规范化输入和符号保留。编辑后答案中的第二个版本应该可以通过您的测试。 - Laurence Gonsalves
1
第二个版本对我也不起作用。例如尝试使用350和0。它应该返回-10,但实际上返回了-350。 - kjyv
@kjyv 我无法重现你所描述的行为。你能发布一下确切的代码吗? - Laurence Gonsalves
啊,抱歉。我再次使用Python测试了您的弧度和角度版本,它运行良好。所以可能是我翻译成C#时出现了错误(我已经没有了)。 - kjyv
2
请注意,从Python 3开始,您实际上可以本地使用tau!只需编写 from math import tau - mhartl
假设(x,y)=(targetA,source A)。它适用于[-180,180]范围内的角度吗?尝试了以下坐标:(-2,-4),(172,-4),(-4,172),(-172,-170),得到的答案符号相反。还是它假定另一种方式?(x,y)=(sourceA,targetA)? - AshlinJP

15

一个在C++中高效运行的代码,适用于任何角度且支持弧度和角度:

inline double getAbsoluteDiff2Angles(const double x, const double y, const double c)
{
    // c can be PI (for radians) or 180.0 (for degrees);
    return c - fabs(fmod(fabs(x - y), 2*c) - c);
}

在此处查看它的运作方式:https://www.desmos.com/calculator/sbgxyfchjr

对于带符号角度:

return fmod(fabs(x - y) + c, 2*c) - c;

在一些其他编程语言中,负数的取模为正,可以省略内部绝对值。


3
很好的想法,但它不能产生有符号角度。 - Marcin Żmigrodzki

11

我挑战自己来提供带有符号的答案:

def f(x,y):
  import math
  return min(y-x, y-x+2*math.pi, y-x-2*math.pi, key=abs)

1
啊...顺便说一下,答案是一个Python函数。抱歉,我刚才在Python模式下。希望没关系。 - David Jones
我将把新公式插入到我的代码中,然后看看它的效果如何!(谢谢 ^_^) - Tom J Nowell
1
我非常确定PeterB的答案也是正确的。而且邪恶的黑客风格。 :) - David Jones
4
这个没有三角函数 :) - nornagon
最好在文件开头导入 math 模块。 - astroide

6

2
没有签名的输出。 - kjyv

5

算术(相对于算法)解法:

angle = Pi - abs(abs(a1 - a2) - Pi);

6
这未能通过测试套件 https://gist.github.com/bradphelan/7fe21ad8ebfcb43696b8 - bradgonesurfing
如果 abs(a1-a2) >>> 360,则会失败。请改用此链接:https://dev59.com/1HI-5IYBdhLWcg3wZ3UR#52432897 - Adriel Jr

2

我非常喜欢Peter B上面的答案,但是如果你需要一个简单易懂的方法来实现相同的结果,这里有一个:

function absAngle(a) {
  // this yields correct counter-clock-wise numbers, like 350deg for -370
  return (360 + (a % 360)) % 360;
}

function angleDelta(a, b) {
  // https://gamedev.stackexchange.com/a/4472
  let delta = Math.abs(absAngle(a) - absAngle(b));
  let sign = absAngle(a) > absAngle(b) || delta >= 180 ? -1 : 1;
  return (180 - Math.abs(delta - 180)) * sign;
}

// sample output
for (let angle = -370; angle <= 370; angle+=20) {
  let testAngle = 10;
  console.log(testAngle, "->", angle, "=", angleDelta(testAngle, angle));
}

需要注意的一点是,我故意反转了符号:逆时针增加的角度是负数,顺时针增加的角度是正数。


0
老掉牙的帖子,但是...我在Desmos中遇到了同样的问题。这是我的实现,如果对任何人有用的话:
LaTex的截图: https://istack.dev59.com/6RzDA.webp 参数v1和v2是向量。像点一样插入。 例如:v1 = (0,0),v2 = (0,1)。
我曾经想过可能有一些方法可以通过复数乘法进行优化,但是我只是在Desmos上随便拼凑一个模型,所以就不管它了。

-1

没有必要计算三角函数。C语言中的简单代码是:

#include <math.h>
#define PIV2 M_PI+M_PI
#define C360 360.0000000000000000000
double difangrad(double x, double y)
{
double arg;

arg = fmod(y-x, PIV2);
if (arg < 0 )  arg  = arg + PIV2;
if (arg > M_PI) arg  = arg - PIV2;

return (-arg);
}
double difangdeg(double x, double y)
{
double arg;
arg = fmod(y-x, C360);
if (arg < 0 )  arg  = arg + C360;
if (arg > 180) arg  = arg - C360;
return (-arg);
}

计算两个数的差,以弧度为单位:

dif = difangrad(a,b);

计算 a 和 b 的差值,以度为单位。

dif = difangdeg(a,b);

difangdeg(180.000000 , -180.000000) = 0.000000
difangdeg(-180.000000 , 180.000000) = -0.000000
difangdeg(359.000000 , 1.000000) = -2.000000
difangdeg(1.000000 , 359.000000) = 2.000000

没有正弦,没有余弦,没有正切,……只有几何!!!


9
Bug!由于您将PIV2定义为"M_PI+M_PI"而不是"(M_PI+M_PI)",所以行arg = arg - PIV2;会扩展为arg = arg - M_PI + M_PI,因此没有任何作用。 - canton7

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