2Dベクトルを最も近い8方向コンパス方向に変換する最良の方法は何ですか?


17

xとyで表される2Dベクトルがある場合、それを最も近いコンパス方向に変換する良い方法は何ですか?

例えば

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

あなたはそれを文字列または列挙型として欲しいですか?(はい、重要)
フィリップ

どちらも、両方の方法で使用されるためです:)私が選ぶ必要がある場合、私は文字列を取るでしょう
izb

1
パフォーマンスも心配ですか、それとも簡潔さだけですか?
マルシンセレディンスキー

2
var angle = Math.atan2(y、x); return <Direction> Math.floor((Math.round(angle /(2 * Math.PI / 8))+ 8 + 2)%8); 私はこれを使用します
鬼丸

簡潔:表現または記述の簡潔さでマーク:すべての精緻化および不必要な詳細から解放されます。ただそこにそれを投げる
...-ディアロック

回答:


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分に変換します。最後に、8を法としてそれを減らしてラップアラウンドを処理し、0と8が両方とも正しく東にマッピングされるようにします。

+ 8をスキップした理由は、一部の言語でatan2()は負の結果を返す場合があります(つまり、0からではなくから+ π)ため、モジュロ演算子()は負の引数(または負の引数に対する動作は未定義の場合があります)。追加する他の方法で結果に影響を与えずに、引数は常にポジティブであることを、還元性を保証する前に入力する(すなわち、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)代わりに北から始まる時計回りの順序でコンパスの見出しをリストした場合も機能します。
イルマリカロネン

2
ちなみに、これは最も簡単で厳密な答えだと思います。
サムホセバー

1
@TheLima:octant = round(8 * angle / 360 + 8) % 8
イルマリカロネン

1
これは4ウェイのcompass:に簡単に変換でき、quadtant = round(4 * angle / (2*PI) + 4) % 4enum:を使用できることに注意してください{ E, N, W, S }
スポイケ

10

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つに分割されます。


4
「角度に変換」する簡単な方法はを使用することですatan2。ただし、0度はおそらく北ではなく東であることに注意してください。
テトラッド

1
angle >=上記のコードのチェックは不要です。たとえば、角度が45より小さい場合、北はすでに返されているため、東のチェックで角度> = 45であるかどうかをチェックする必要はありません。同様に、西に戻る前にチェックをする必要はまったくありません。これが唯一の可能性です。
MrKWatkins

4
私はこれを方向を知るための簡潔な方法とは呼びません。かなり不格好なようで、これを異なる「解像度」に適応させるには多くの変更が必要です。if16か所以上の方向に行きたい場合は、大量の発言をしないでください。
bummzack

2
ベクトルを正規化する必要はありません。角度は大きさの変化に対して同じままです。
カイロタン

@bummzackに感謝します。列挙値を追加するだけで、より簡潔で簡単に精度を上げることができるように、投稿を編集しました。
マイケルハウス

8

ベクトルを扱う場合は、特定のフレームで角度に変換するのではなく、基本的なベクトル操作を検討してください。

クエリベクトル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];
    }    
}

2
この実装は、あまり手間をかけずにブランチレスでベクトル化することもできます。
13

1
キーとして?これはあまり深刻ではありません。mapfloat2
サムホセバー

それは教訓的な方法での「擬似コード」です。パニックに最適化された実装が必要な場合、GDSEはおそらくコピーパスタの場所ではありません。float2をキーとして使用する場合、floatはここで使用する整数全体を正確に表すことができ、完全に優れたコンパレータを作成できます。浮動小数点キーは、特別な値が含まれているか、計算結果を検索しようとする場合にのみ不適切です。連想シーケンスの繰り返しは問題ありません。配列で線形検索を使用することもできますが、それは無意味です。
ラースヴィクルンド

3

ここで言及されていない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つ以上のベクトルの正味の見出しを取得する場合は、通常の方法でベクトル計算を実行できます。(複素数として、加算するのではなく、結果を乗算します)


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

ここで複素数を使用する別の方法は、複素数の乗算に回転が含まれることを観察することです。円の周りの回転の1/8を表す複素数を作成すると、それを乗算するたびに1オクタント移動します。それで、あなたは尋ねることができます:東から現在の見出しに行くのにどれくらいの乗算がかかったかを数えますか?「これを何回掛ける必要があるか」に対する答えは対数です。複素数の対数を調べると…atan2を使用します。したがって、これは最終的にIlmariの答えと同等になります。
amitp

-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"};
}

なぜこれが投票されているのですか?
レイタイエク

ほとんどの場合、コードの背後に説明がないためです。なぜこれが解決策であり、どのように機能するのですか?
ベイランクール

実行しましたか?
レイタイエク

いいえ、クラスの名前が与えられたので、私はあなたがやったと思い、うまくいったと思います。そしてそれは素晴らしいことです。しかし、あなたはなぜ人々が投票するのかと尋ね、私は答えました。私はそれが機能しなかったことを暗示しませんでした:)
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)

今のところ、これはあまり意味のない文字の集まりです。なぜこれが質問に対して有効な解決策であるのか、それはどのように機能するのですか?
クール

私はjn excelを書いたように式を書いて、完璧に働いています。
セオドアパナゴス

= 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-branch に入れることですが、読みやすさを害するため、それを控えました。


2
申し訳ありませんが、探している答えが正確に得られるわけではありません。このコードを使用すると、ベクトルが正確に北の場合にのみ「N」、xが他の値の場合にNEまたはNWが生成されます。ベクトルは近いNへのNWよりも、それはNが得られるであれば何が必要なのは例えば、最も近いコンパスの方向である
izb

これは実際に最も近い方向を示しますか?(0.00001,100)のベクトルが北東を与えるようです。編集:あなたは私にそれを打ち負かしたizb。
CiscoIPPhone

あなたはあなたが最も近い方向が欲しいとは言わなかった。
フィリップ

1
申し訳ありませんが、タイトルにそれを隠しました。質問の本文で明確にすべき
でした

1
無限のノルムを使用するのはどうですか?max(abs(vector.components))で除算すると、そのノルムに関して正規化されたベクトルが得られます。これでif (x > 0.9) dir |= DIR_E、残りすべてに基づいて小さなチェックテーブルを作成できます。Phillippの元のコードよりも優れており、L2ノルムとatan2を使用するよりも少し安いはずです。たぶん..またはそうでないかもしれません。
テオドロン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.