Qual è il modo migliore di trasformare un vettore 2D nella direzione della bussola a 8 vie più vicina?


17

Se hai un vettore 2D espresso come xey, qual è un buon modo per trasformarlo nella direzione della bussola più vicina?

per esempio

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

lo vuoi come una stringa o un enum? (sì, è importante)
Philipp,

In entrambi i casi, poiché verrà utilizzato in entrambi i modi :) Anche se dovessi scegliere, prenderei una stringa.
IZB

1
Sei preoccupato anche per la performance o solo per la concisione?
Marcin Seredynski,

2
angolo var = Math.atan2 (y, x); return <Direction> Math.floor ((Math.round (angolo / (2 * Math.PI / 8)) + 8 + 2)% 8); Uso questo
Kikaimaru il

Conciso: caratterizzato da brevità di espressione o affermazione: libero da ogni elaborazione e dettaglio superfluo. Lo sto solo lanciando ...
Dialock,

Risposte:


25

Il modo più semplice è probabilmente ottenere l'angolo del vettore usando atan2(), come suggerisce Tetrad nei commenti, e quindi ridimensionarlo e arrotondarlo, ad esempio (pseudocodice):

// 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];

La octant = round( 8 * angle / (2*PI) + 8 ) % 8linea potrebbe richiedere qualche spiegazione. In quasi tutte le lingue che conosco ce l'hanno, ilatan2() funzione restituisce l'angolo in radianti. Dividendolo per 2 π lo converte da radianti a frazioni di un cerchio completo, e moltiplicandolo per 8 lo converte quindi in ottavi di cerchio, che arrotondiamo quindi all'intero più vicino. Infine, lo riduciamo modulo 8 per occuparci dell'involucro, in modo che entrambi 0 e 8 siano correttamente mappati ad est.

Il motivo per + 8cui, che ho saltato sopra, è che in alcune lingue atan2()possono restituire risultati negativi (cioè da - π a + π anziché da 0 a 2 π ) e l'operatore modulo ( %) può essere definito per restituire valori negativi per argomenti negativi (o il suo comportamento per argomenti negativi può essere indefinito). L'aggiunta 8(ovvero un giro completo) all'input prima della riduzione garantisce che gli argomenti siano sempre positivi, senza influire in alcun modo sul risultato.

Se la tua lingua non fornisce una comoda funzione di arrotondamento al più vicino, puoi invece utilizzare una conversione di numeri interi troncata e aggiungere semplicemente 0,5 all'argomento, in questo modo:

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

Si noti che, in alcune lingue, la conversione da float a intero predefinita arrotonda gli input negativi verso zero anziché verso il basso, che è un altro motivo per assicurarsi che l'input sia sempre positivo.

Ovviamente, puoi sostituire tutte le occorrenze di 8quella linea con un altro numero (ad esempio 4 o 16, o anche 6 o 12 se ti trovi su una mappa esadecimale) per dividere il cerchio in così tante direzioni. Basta regolare l'enum / array di conseguenza.


Si noti che di solito atan2(y,x)non lo è atan2(x,y).
Sam Hocevar,

@ Sam: Oops, corretto. Naturalmente, atan2(x,y)funzionerebbe anche se si elencassero le intestazioni della bussola in senso orario a partire da nord.
Ilmari Karonen,

2
+1 a proposito, penso davvero che questa sia la risposta più semplice e rigorosa.
Sam Hocevar,

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

1
Do atto che questo può essere facilmente convertito in una bussola a 4 vie: quadtant = round(4 * angle / (2*PI) + 4) % 4e l'utilizzo di enum: { E, N, W, S }.
Spoike,

10

Hai 8 opzioni (o 16 o più se vuoi una precisione ancora più fine).

enter image description here

Usa atan2(y,x)per ottenere l'angolo per il tuo vettore.

atan2() funziona nel modo seguente:

enter image description here

Quindi x = 1, y = 0 si tradurrà in 0, ed è discontinuo in x = -1, y = 0, contenente sia π che -π.

Ora dobbiamo solo mappare l'output di atan2() per abbinare quello della bussola che abbiamo sopra.

Probabilmente il più semplice da implementare è un controllo incrementale degli angoli. Ecco alcuni pseudo codici che possono essere facilmente modificati per una maggiore precisione:

//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]

Ora per aggiungere più precisione, aggiungi semplicemente i valori alla direzione enum.

L'algoritmo funziona controllando i valori crescenti attorno alla bussola per vedere se il nostro angolo si trova da qualche parte tra il punto in cui abbiamo verificato l'ultima volta e la nuova posizione. Ecco perché iniziamo da -PI + incremento / 2. Vogliamo compensare i nostri controlli per includere lo stesso spazio attorno a ciascuna direzione. Qualcosa come questo:

enter image description here

L'ovest è diviso in due a causa dei valori di ritorno di atan2()ad ovest sono discontinui.


4
Un modo semplice per "convertirli in un angolo" è usare atan2, sebbene tieni presente che probabilmente 0 gradi sarebbe est e non nord.
Tetrad,

1
Non hai bisogno dei angle >=controlli nel codice sopra; ad esempio se l'angolo è inferiore a 45, il nord sarà già stato restituito, quindi non è necessario verificare se angolo> = 45 per il controllo est. Allo stesso modo non è necessario alcun controllo prima di tornare a ovest - è l'unica possibilità rimasta.
MrKWatkins,

4
Non lo definirei un modo conciso per ottenere la direzione. Sembra piuttosto goffo e richiederà molte modifiche per adattarlo a diverse "risoluzioni". Non parlare di un sacco di ifaffermazioni se vuoi andare per 16 o più direzioni.
Bummzack,

2
Non è necessario normalizzare il vettore: l'angolo rimane invariato rispetto ai cambiamenti di grandezza.
Kylotan,

Grazie a @bummzack, ho modificato il post per rendere più conciso e facile aumentare la precisione semplicemente aggiungendo più valori enum.
MichaelHouse

8

Ogni volta che hai a che fare con vettori, considera le operazioni vettoriali fondamentali invece di convertirle in angoli in un determinato frame.

Dato un vettore di query ve un insieme di vettori di unità s, il vettore più allineato è il vettore s_iche massimizza dot(v,s_i). Ciò è dovuto al fatto che il prodotto a punti con lunghezze fisse per i parametri ha un massimo per i vettori con la stessa direzione e un minimo per i vettori con direzioni opposte, cambiando uniformemente tra di loro.

Questo si generalizza banalmente in più dimensioni di due, è estensibile con direzioni arbitrarie e non soffre di problemi specifici del frame come infiniti gradienti.

Dal punto di vista dell'implementazione, ciò si riduce ad associare da un vettore in ciascuna direzione cardinale a un identificatore (enum, stringa, qualunque cosa tu abbia bisogno) che rappresenti quella direzione. Dovresti quindi scorrere il tuo set di direzioni, trovando quello con il punto più alto prodotto.

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
Questa implementazione può anche essere scritta senza rami e vettorializzata senza troppi problemi.
Promessa del

1
A mapcon float2come chiave? Questo non sembra molto serio.
Sam Hocevar,

È "pseudo-codice" in modo didattico. Se desideri implementazioni ottimizzate per il panico, GDSE non è probabilmente il posto dove andare per la tua copia-pasta. Per quanto riguarda l'utilizzo di float2 come chiave, un float può rappresentare esattamente tutti i numeri che usiamo qui, e puoi fare un comparatore perfettamente adatto a loro. Le chiavi in ​​virgola mobile non sono adatte solo se contengono valori speciali o si tenta di cercare i risultati calcolati. Andare avanti su una sequenza associativa va bene. Avrei potuto usare una ricerca lineare in un array, certo, ma sarebbe stato un disordine inutile.
Lars Viklund,

3

Un modo che non è stato menzionato qui è trattare i vettori come numeri complessi. Non richiedono trigonometria e possono essere piuttosto intuitivi per l'aggiunta, la moltiplicazione o l'arrotondamento delle rotazioni, soprattutto perché hai già le intestazioni rappresentate come coppie di numeri.

Nel caso in cui non si abbia familiarità con esse, le direzioni sono espresse nella forma di a + b (i) con a essere il componente reale e b (i) è l'immaginario. Se immagini il piano cartesiano con la X reale e Y immaginaria, 1 sarebbe est (a destra), sarei a nord.

Ecco la parte chiave: le 8 direzioni cardinali sono rappresentate esclusivamente con i numeri 1, -1 o 0 per le loro componenti reali e immaginarie. Quindi tutto ciò che devi fare è ridurre le tue coordinate X, Y come rapporto e arrotondare entrambe al numero intero più vicino per ottenere la direzione.

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

Per la conversione diagonale verso la direzione più vicina, riduci proporzionalmente X e Y in modo che il valore maggiore sia esattamente 1 o -1. Impostato

// 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));
}

Arrotondare entrambi i componenti di ciò che era originariamente (10, -2) ti dà 1 + 0 (i) o 1. Quindi la direzione più vicina è est.

Quanto sopra in realtà non richiede l'uso di una struttura numerica complessa, ma pensarli come tali rende più veloce trovare le 8 direzioni cardinali. Puoi fare matematica vettoriale nel solito modo se vuoi ottenere l'intestazione netta di due o più vettori. (Come numeri complessi, non si aggiunge, ma si moltiplica per il risultato)


1
Questo è fantastico, ma fa un errore simile a quello che ho fatto nel mio tentativo. Le risposte sono vicine ma non giuste. L'angolo di confine tra E e NE è di 22,5 gradi, ma questo si interrompe a 26,6 gradi.
IZB

Max(x, y)dovrebbe essere Max(Abs(x, y))lavorare per i quadranti negativi. L'ho provato e ho ottenuto lo stesso risultato di izb: questo cambia le direzioni della bussola con gli angoli sbagliati. Immagino che cambierebbe quando header.y / header.x incrocia 0,5 (quindi il valore arrotondato passa da 0 a 1), che è arctan (0,5) = 26,565 °.
tra il

Un modo diverso di usare numeri complessi qui è osservare che la moltiplicazione di numeri complessi comporta una rotazione. Se costruisci un numero complesso che rappresenta 1/8 di una rotazione attorno a un cerchio, ogni volta che lo moltiplichi per esso, sposti un ottante. Quindi potresti chiedere: possiamo contare quante moltiplicazioni ci sono voluti per andare dall'est all'attuale rotta? La risposta a "quante volte dobbiamo moltiplicare per questo" è un logaritmo . Se cerchi logaritmi per numeri complessi ... usa atan2. Quindi questo finisce per essere equivalente alla risposta di Ilmari.
tra il

-2

questo sembra funzionare:

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

perché questo viene votato?
Ray Tayek,

Molto probabilmente perché non ci sono spiegazioni dietro il tuo codice. Perché questa è la soluzione e come funziona?
Vaillancourt

l'hai eseguito?
Ray Tayek,

No, e dato il nome della classe, ho pensato che lo facessi e ha funzionato. Ed è fantastico. Ma hai chiesto perché la gente ha votato in basso e io ho risposto; Non ho mai sottinteso che non funzionasse :)
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 + segno (x)) * (1 segno (y ^ 2)) - (2 + segno (x)) * segno (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)

Per ora, questo è solo un gruppo di personaggi che non ha molto senso; perché questa è una soluzione che funzionerebbe per la domanda, come funziona?
Vaillancourt

Scrivo la formula come ho scritto jn excel e funziona perfettamente.
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)
theodore panagos

-4

Quando vuoi una stringa:

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)

Questo ti dà costanti utilizzando bitfield:

// 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

Un leggero miglioramento delle prestazioni sarebbe quello di mettere i <-check nel ramo else dei corrispondenti >-checks, ma mi sono astenuto dal farlo perché danneggia la leggibilità.


2
Ci dispiace, ma questo non darà esattamente la risposta che sto cercando. Con quel codice produrrà "N" solo se il vettore è precisamente a nord, e NE o NW se x è un altro valore. Ciò di cui ho bisogno è la direzione della bussola più vicina, ad esempio se il vettore è più vicino a N di NW, allora produrrà N.
izb

Questo darebbe davvero la direzione più vicina? Sembra che un vettore di (0.00001.100) ti darebbe nord-est. modifica: mi hai battuto ad esso izb.
CiscoIPPhone

non hai detto che vuoi la direzione più vicina.
Philipp,

1
Mi dispiace, l'ho nascosto nel titolo. Avrebbe dovuto essere più chiaro nel corpo
dell'interrogazione

1
Che dire dell'utilizzo della norma infinita? La divisione per max (abs (vector.components)) ti dà un vettore normalizzato rispetto a quella norma. Ora puoi scrivere una piccola tabella di controllo basata su if (x > 0.9) dir |= DIR_Ee tutto il resto. Dovrebbe essere migliore del codice originale di Phillipp e un po 'più economico rispetto all'utilizzo della norma L2 e atan2. Forse o forse no.
teodron,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.