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
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
Risposte:
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 ) % 8
linea 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 + 8
cui, 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 8
quella 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.
atan2(y,x)
non lo è atan2(x,y)
.
atan2(x,y)
funzionerebbe anche se si elencassero le intestazioni della bussola in senso orario a partire da nord.
octant = round(8 * angle / 360 + 8) % 8
quadtant = round(4 * angle / (2*PI) + 4) % 4
e l'utilizzo di enum: { E, N, W, S }
.
Hai 8 opzioni (o 16 o più se vuoi una precisione ancora più fine).
Usa atan2(y,x)
per ottenere l'angolo per il tuo vettore.
atan2()
funziona nel modo seguente:
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:
L'ovest è diviso in due a causa dei valori di ritorno di atan2()
ad ovest sono discontinui.
atan2
, sebbene tieni presente che probabilmente 0 gradi sarebbe est e non nord.
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.
if
affermazioni se vuoi andare per 16 o più direzioni.
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 v
e un insieme di vettori di unità s
, il vettore più allineato è il vettore s_i
che 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];
}
}
map
con float2
come chiave? Questo non sembra molto serio.
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)
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 °.
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"};
}
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)
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à.
if (x > 0.9) dir |= DIR_E
e 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.