如果您将2D向量表示为x和y,将其转换为最接近的罗盘方向的好方法是什么?
例如
x:+1, y:+1 => NE
x:0, y:+3 => N
x:+10, y:-2 => E // closest compass direction
如果您将2D向量表示为x和y,将其转换为最接近的罗盘方向的好方法是什么?
例如
x:+1, y:+1 => NE
x:0, y:+3 => N
x:+10, y:-2 => E // closest compass direction
Answers:
最简单的方法可能是使用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)
。
atan2(x,y)
,如果只是从北开始按顺时针顺序列出罗盘标题,那也可以。
octant = round(8 * angle / 360 + 8) % 8
quadtant = round(4 * angle / (2*PI) + 4) % 4
使用枚举:{ E, N, W, S }
。
您有8个选项(如果需要更高的精度,则有16个或更多)。
使用atan2(y,x)
让您的向量的夹角。
atan2()
以下列方式工作:
所以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开始。我们想抵消支票,以在每个方向上包含相等的空间。像这样:
由于atan2()
at 的返回值不连续,所以West分为两部分。
atan2
,尽管请记住0度可能是东而不是北。
angle >=
上面的代码中的检查;例如,如果角度小于45度,那么北角将已经返回,因此您无需检查东向检查角是否大于等于45度。同样,返回西部之前,您根本不需要任何检查-这是唯一的可能性。
if
如果要进行16个或更多的方向操作,不要说很多语句。
每当处理向量时,都应考虑基本向量运算,而不要转换为某些特定帧中的角度。
给定查询向量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];
}
}
map
有float2
作为的关键?这看起来不是很严重。
在此未提及的一种方法是将向量视为复数。它们不需要三角函数,并且可以很直观地进行旋转的加,乘或舍入运算,尤其是因为您已经将标题表示为数字对了。
如果您不熟悉它们,则方向以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个基本方向。如果要获得两个或多个向量的净航向,则可以按通常的方式进行向量数学运算。(作为复数,您不必加,但要乘以结果)
Max(x, y)
应该Max(Abs(x, y))
为负象限工作。我尝试了一下,并得到了与izb相同的结果-这会将罗盘方向切换到错误的角度。我猜想当heading.y / heading.x越过0.5(因此舍入后的值从0切换到1)时,它会切换,即arctan(0.5)= 26.565°。
这似乎可行:
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"};
}
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)
当您想要一个字符串时:
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分支中,但是我不这样做,因为这会损害可读性。
if (x > 0.9) dir |= DIR_E
其余所有内容编写一个小的检查表。它应该比Phillipp的原始代码更好,并且比使用L2规范和atan2便宜一些。也许..也许不是。