xとyで表される2Dベクトルがある場合、それを最も近いコンパス方向に変換する良い方法は何ですか?
例えば
x:+1, y:+1 => NE
x:0, y:+3 => N
x:+10, y:-2 => E // closest compass direction
xとyで表される2Dベクトルがある場合、それを最も近いコンパス方向に変換する良い方法は何ですか?
例えば
x:+1, y:+1 => NE
x:0, y:+3 => N
x:+10, y:-2 => E // closest compass direction
回答:
最も簡単な方法は、おそらく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分に変換します。最後に、8を法としてそれを減らしてラップアラウンドを処理し、0と8が両方とも正しく東にマッピングされるようにします。
+ 8
をスキップした理由は、一部の言語でatan2()
は負の結果を返す場合があります(つまり、0から2πではなく-πから+ π)ため、モジュロ演算子()は負の引数(または負の引数に対する動作は未定義の場合があります)。追加する他の方法で結果に影響を与えずに、引数は常にポジティブであることを、還元性を保証する前に入力する(すなわち、1つの完全なターン)。%
8
あなたの言語が便利な四捨五入機能を提供しない場合は、代わりに切り捨て整数変換を使用し、次のように引数に0.5を追加するだけです。
int octant = int( 8 * angle / (2*PI) + 8.5 ) % 8; // int() rounds down
一部の言語では、デフォルトの浮動小数点から整数への変換は負の入力を切り捨てるのではなくゼロに切り上げることに注意してください。これは、入力が常に正であることを確認するもう1つの理由です。
もちろん、8
その行のすべての出現を他の番号(たとえば、16進マップの場合は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
enum:を使用できることに注意してください{ 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 + increment / 2から始めます。チェックをオフセットして、各方向の周囲に等しいスペースを含めたいと思います。このようなもの:
Westの戻り値atan2()
は不連続であるため、Westは2つに分割されます。
atan2
。ただし、0度はおそらく北ではなく東であることに注意してください。
angle >=
上記のコードのチェックは不要です。たとえば、角度が45より小さい場合、北はすでに返されているため、東のチェックで角度> = 45であるかどうかをチェックする必要はありません。同様に、西に戻る前にチェックをする必要はまったくありません。これが唯一の可能性です。
if
16か所以上の方向に行きたい場合は、大量の発言をしないでください。
ベクトルを扱う場合は、特定のフレームで角度に変換するのではなく、基本的なベクトル操作を検討してください。
クエリベクトルv
と単位ベクトルのセットが与えられると、s
最も位置合わせされたベクトルはs_i
を最大化するベクトルになりdot(v,s_i)
ます。これは、パラメーターに固定長を与えられた内積は、同じ方向のベクトルに対して最大値を持ち、反対方向のベクトルに対して最小値を持ち、その間で滑らかに変化するためです。
これは2つ以上の次元に簡単に一般化し、任意の方向で拡張可能であり、無限勾配のようなフレーム固有の問題を被りません。
実装面では、これは各方向のベクトルから、その方向を表す識別子(enum、string、必要なもの)に関連付けることに要約されます。次に、一連の方向をループして、最高の内積を持つ方向を見つけます。
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
ここで言及されていない1つの方法は、ベクトルを複素数として扱うことです。それらは三角法を必要とせず、特に数字のペアとして表される見出しをすでに持っているので、回転を追加、乗算、または丸めるために非常に直感的です。
それらに慣れていない場合、方向はa + b(i)の形式で表現され、aは実数成分であり、b(i)は虚数です。Xが実数でYが虚数であるデカルト平面を想像すると、1が東(右)になり、iが北になります。
重要な部分は次のとおりです。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つの基本方向をすばやく見つけることができます。2つ以上のベクトルの正味の見出しを取得する場合は、通常の方法でベクトル計算を実行できます。(複素数として、加算するのではなく、結果を乗算します)
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-branch に入れることですが、読みやすさを害するため、それを控えました。
if (x > 0.9) dir |= DIR_E
、残りすべてに基づいて小さなチェックテーブルを作成できます。Phillippの元のコードよりも優れており、L2ノルムとatan2を使用するよりも少し安いはずです。たぶん..またはそうでないかもしれません。