将2D向量转换为最接近的8向罗盘方向的最佳方法是什么?


17

如果您将2D向量表示为x和y,将其转换为最接近的罗盘方向的好方法是什么?

例如

x:+1,  y:+1 => NE
x:0,   y:+3 => N
x:+10, y:-2 => E   // closest compass direction

您想要将其作为字符串还是枚举?(是的,这很重要)
Philipp

可以,因为两种方法都可以使用它。
izb

1
您是否也关注性能,还是仅关注简洁性?
Marcin Seredynski 2013年

2
var angle = Math.atan2(y,x); 返回<Direction> Math.floor((Math.round(angle /(2 * Math.PI / 8))+ 8 + 2)%8); 我使用这个
Kikaimaru

简洁:以简短的表达或陈述为标志:没有任何详尽的描述和多余的细节。只是把那个扔出去...
Dialock

Answers:


25

最简单的方法可能是使用atan2()Tetrad在注释中建议的,使用来获取向量的角度,然后对其进行缩放和舍入,例如(伪代码):

// enumerated counterclockwise, starting from east = 0:
enum compassDir {
    E = 0, NE = 1,
    N = 2, NW = 3,
    W = 4, SW = 5,
    S = 6, SE = 7
};

// for string conversion, if you can't just do e.g. dir.toString():
const string[8] headings = { "E", "NE", "N", "NW", "W", "SW", "S", "SE" };

// actual conversion code:
float angle = atan2( vector.y, vector.x );
int octant = round( 8 * angle / (2*PI) + 8 ) % 8;

compassDir dir = (compassDir) octant;  // typecast to enum: 0 -> E etc.
string dirStr = headings[octant];

octant = round( 8 * angle / (2*PI) + 8 ) % 8行可能需要一些解释。在我所知道的几乎所有语言中,atan2()函数均以弧度返回角度。除以2个它π其转换从弧度到一个完整的圆的级分,并通过8相乘然后将其转换为一个圆的八分,我们然后舍入到最接近的整数。最后,我们将其模8减少以处理环绕,以便将0和8正确映射到东部。

的原因+ 8,这是我跳过过去的上方,是在某些语言中atan2()可能返回否定结果(即,从- π到+ π,而不是从0至2 π)和模运算符(%)可以被定义为返回负值为否定参数(或其对否定参数的行为可能未定义)。新增中8减少之前在输入上(即整圈)可确保参数始终为正,而不会以任何其他方式影响结果。

如果您的语言没有提供便捷的舍入到最接近函数,则可以使用截断整数转换,而只需在参数中加上0.5,如下所示:

int octant = int( 8 * angle / (2*PI) + 8.5 ) % 8;  // int() rounds down

请注意,在某些语言中,默认的浮点到整数转换将负输入向上舍入为零而不是向下舍入,这是确保输入始终为正的另一个原因。

当然,您可以用8其他一些数字代替该行上所有出现的数字(例如,如果您在十六进制地图上,则为4或16,或者甚至是6或12),以将圆分成多个方向。只需相应地调整枚举/数组即可。


请注意,通常atan2(y,x)不是atan2(x,y)
sam hocevar

@Sam:糟糕,已更正。当然atan2(x,y),如果只是从北开始按顺时针顺序列出罗盘标题,那也可以。
Ilmari Karonen

2
+1,我真的认为这是最简单明了的答案。
sam hocevar

1
@TheLima:octant = round(8 * angle / 360 + 8) % 8
Ilmari Karonen

1
注意,这可以很容易地转换成4路罗盘:quadtant = round(4 * angle / (2*PI) + 4) % 4使用枚举:{ E, N, W, S }
Spoike '16

10

您有8个选项(如果需要更高的精度,则有16个或更多)。

enter image description here

使用atan2(y,x)让您的向量的夹角。

atan2() 以下列方式工作:

enter image description here

所以x = 1,y = 0将得到0,并且在x = -1,y = 0处不连续,同时包含π和-π。

现在,我们只需要映射输出atan2()以匹配上面的指南针。

可能最容易实现的是角度递增检查。以下是一些伪代码,可以很容易地对其进行修改以提高精度:

//start direction from the lowest value, in this case it's west with -π
enum direction {
west,
south,
east,
north
}

increment = (2PI)/direction.count
angle = atan2(y,x);
testangle = -PI + increment/2
index = 0

while angle > testangle
    index++
    if(index > direction.count - 1)
        return direction[0] //roll over
    testangle += increment


return direction[index]

现在要增加精度,只需将值添加到方向枚举即可。

该算法通过检查罗盘周围的增加值来工作,以查看我们的角度是否位于上次检查的位置与新位置之间的某个位置。这就是为什么我们从-PI +增量/ 2开始。我们想抵消支票,以在每个方向上包含相等的空间。像这样:

enter image description here

由于atan2()at 的返回值不连续,所以West分为两部分。


4
使用“将它们转换为角度”的一种简单方法是使用atan2,尽管请记住0度可能是东而不是北。
Tetrad

1
您不需要angle >=上面的代码中的检查;例如,如果角度小于45度,那么北角将已经返回,因此您无需检查东向检查角是否大于等于45度。同样,返回西部之前,您根本不需要任何检查-这是唯一的可能性。
MrKWatkins

4
我不会将其称为一种简明的方法来指明方向。似乎比较笨拙,需要进行大量更改才能使其适应不同的“分辨率”。if如果要进行16个或更多的方向操作,不要说很多语句。
bummzack

2
无需对向量进行归一化:角度随幅度的变化而保持不变。
Kylotan

感谢@bummzack,我对帖子进行了编辑,使其更加简洁,仅通过添加更多枚举值即可轻松提高精度。
MichaelHouse

8

每当处理向量时,都应考虑基本向量运算,而不要转换为某些特定帧中的角度。

给定查询向量v和一组单位向量s,最对齐的向量是s_i最大化的向量dot(v,s_i)。这是因为给定参数固定长度的点积对于具有相同方向的矢量具有最大值,而具有相反方向的矢量具有最小值,从而在它们之间平滑地变化。

这将平凡地推广到比二维更多的维度,可以在任意方向上扩展,并且不会遇到诸如无限梯度之类的特定于框架的问题。

在实现方面,这将归结为将向量从每个基本方向上与代表该方向的标识符(枚举,字符串,无论需要什么)相关联。然后,您将遍历一组方向,找到具有最高点积的方向。

map<float2,Direction> candidates;
candidates[float2(1,0)] = E; candidates[float2(0,1)] = N; // etc.

for each (float2 dir in candidates)
{
    float goodness = dot(dir, v);
    if (goodness > bestResult)
    {
        bestResult = goodness;
        bestDir = candidates[dir];
    }    
}

2
该实现也可以无分支地编写和向量化而不会带来太多麻烦。
允许

1
一个mapfloat2作为的关键?这看起来不是很严重。
sam hocevar

它是一种有说服力的“伪代码”。如果您需要紧急优化的实现,则GDSE可能不是复制粘贴的理想之地。至于使用float2作为键,float可以精确表示我们在此使用的整数,并且您可以为它们创建一个完美的比较器。仅当浮点键包含特殊值或您尝试查找计算结果时,才不合适。在关联序列上进行迭代是可以的。当然,我本可以在数组中使用线性搜索,但这只会毫无意义。
Lars Viklund

3

在此未提及的一种方法是将向量视为复数。它们不需要三角函数,并且可以很直观地进行旋转的加,乘或舍入运算,尤其是因为您已经将标题表示为数字对了。

如果您不熟悉它们,则方向以a + b(i)的形式表示,其中a为实数分量,b(i)为虚数。如果您想象X是实数,Y是虚数的笛卡尔平面,则1将是东(右),我将是北。

这是关键部分:8个基本方向分别用数字1,-1或0表示其实部和虚部。因此,您要做的就是将X,Y坐标按比例缩小,然后将它们四舍五入为最接近的整数以得到方向。

NW (-1 + i)       N (i)        NE (1 + i)
W  (-1)          Origin        E  (1)
SW (-1 - i)      S (-i)        SE (1 - i)

对于航向到最近的对角线转换,请按比例减小X和Y,以使较大的值恰好是1或-1。组

// Some pseudocode

enum xDir { West = -1, Center = 0, East = 1 }
enum yDir { South = -1, Center = 0, North = 1 }

xDir GetXdirection(Vector2 heading)
{
    return round(heading.x / Max(heading.x, heading.y));
}

yDir GetYdirection(Vector2 heading)
{
    return round(heading.y / Max(heading.x, heading.y));
}

四舍五入原先的两个分量(10,-2)得到1 + 0(i)或1。因此最接近的方向是东。

上面的代码实际上并不需要使用复杂的数字结构,但是考虑到它们,可以更快地找到8个基本方向。如果要获得两个或多个向量的净航向,则可以按通常的方式进行向量数学运算。(作为复数,您不必加,但要乘以结果)


1
这真棒,但是犯的错误与我自己尝试犯的错误类似。答案是接近但不正确的。E和NE之间的边界角为22.5度,但以26.6度截止。
izb

Max(x, y)应该Max(Abs(x, y))为负象限工作。我尝试了一下,并得到了与izb相同的结果-这会将罗盘方向切换到错误的角度。我猜想当heading.y / heading.x越过0.5(因此舍入后的值从0切换到1)时,它会切换,即arctan(0.5)= 26.565°。
amitp '18

这里使用复数的另一种方法是观察复数的乘法涉及旋转。如果您构造一个表示围绕一个圆旋转的1/8的复数,则每次乘以它,就移动一个八分圆。因此,您可能会问:我们能算一下从东到当前航向的乘数是多少?“我们必须乘以多少次”的答案是对数。如果您查找复数的对数……它将使用atan2。因此,这最终等同于Ilmari的答案。
阿米特

-2

这似乎可行:

public class So49290 {
    int piece(int x,int y) {
        double angle=Math.atan2(y,x);
        if(angle<0) angle+=2*Math.PI;
        int piece=(int)Math.round(n*angle/(2*Math.PI));
        if(piece==n)
            piece=0;
        return piece;
    }
    void run(int x,int y) {
        System.out.println("("+x+","+y+") is "+s[piece(x,y)]);
    }
    public static void main(String[] args) {
        So49290 so=new So49290();
        so.run(1,0);
        so.run(1,1);
        so.run(0,1);
        so.run(-1,1);
        so.run(-1,0);
        so.run(-1,-1);
        so.run(0,-1);
        so.run(1,-1);
    }
    int n=8;
    static final String[] s=new String[] {"e","ne","n","nw","w","sw","s","se"};
}

为什么这被否决?
Ray Tayek 2015年

最有可能的原因是您的代码背后没有解释。为什么这是解决方案?它如何工作?
Vaillancourt

你跑了吗?
Ray Tayek '19

不,给定班级的名称,我认为您做到了,并且它能起作用。那太好了。但是你问为什么人们投了反对票,我回答了。我从不暗示它不起作用:)
Vaillancourt

-2

E = 0,NE = 1,N = 2,NW = 3,W = 4,SW = 5,S = 6,SE = 7

f(x,y)= mod((4-2 *(1 + sign(x))*(1-sign(y ^ 2))-(2 + sign(x))* sign(y)

    -(1+sign(abs(sign(x*y)*atan((abs(x)-abs(y))/(abs(x)+abs(y))))

    -pi()/(8+10^-15)))/2*sign((x^2-y^2)*(x*y))),8)

目前,这只是一堆没有太大意义的字符。为什么这是一个可以解决该问题的解决方案,它如何起作用?
Vaillancourt

我在写jn excel时写了公式,并且运行良好。
西奥多·帕纳戈斯(Theodore panagos),

= MOD((4-2 *(1 + SIGN(X1))*(1-SIGN(Y1 ^ 2))-(2 + SIGN(X1))* SIGN(Y1)-(1 + SIGN(ABS(SIGN (X1 * Y1)* ATAN((ABS(X1)-ABS(Y1))/(ABS(X1)+ ABS(Y1))))-PI()/(8 + 10 ^ -15)))/ 2 * SIGN((X1 ^ 2-Y1 ^ 2)*(X1 * Y1))),8)
西奥多·帕纳戈斯,

-4

当您想要一个字符串时:

h_axis = ""
v_axis = ""

if (x > 0) h_axis = "E"    
if (x < 0) h_axis = "W"    
if (y > 0) v_axis = "S"    
if (y < 0) v_axis = "N"

return v_axis.append_string(h_axis)

通过使用位域,可以为您提供常量:

// main direction constants
DIR_E = 0x1
DIR_W = 0x2
DIR_S = 0x4
DIR_N = 0x8
// mixed direction constants
DIR_NW = DIR_N | DIR_W    
DIR_SW = DIR_S | DIR_W
DIR_NE = DIR_N | DIR_E
DIR_SE = DIR_S | DIR_E

// calculating the direction
dir = 0x0

if (x > 0) dir |= DIR_E 
if (x < 0) dir |= DIR_W    
if (y > 0) dir |= DIR_S    
if (y < 0) dir |= DIR_N

return dir

性能上的细微改进是将<-checks放入相应的>-checks 的else分支中,但是我不这样做,因为这会损害可读性。


2
抱歉,但是不能完全提供我要的答案。使用该代码,如果向量正好位于北,则仅会产生“ N”;如果x是其他任何值,则仅会产生NE或NW。我需要的是最接近的罗盘方向,例如,如果向量是更接近至N比NW那么它会产生N.
IZB

这实际上会给出最接近的方向吗?看来(0.00001,100)的向量会给您东北。编辑:你打败了我的izb。
CiscoIPPhone

您没有说您想要最接近的方向。
菲利普

1
抱歉,我将其隐藏在标题中。在问题正文中应该更加清楚
izb

1
使用无限范数呢?用max(abs(vector.components))除以相对于该范数的归一化向量。现在,您可以根据if (x > 0.9) dir |= DIR_E其余所有内容编写一个小的检查表。它应该比Phillipp的原始代码更好,并且比使用L2规范和atan2便宜一些。也许..也许不是。
teodron
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.