Jaki jest najlepszy sposób na przekształcenie wektora 2D w najbliższy 8-kierunkowy kierunek kompasu?

17

Jeśli masz wektor 2D wyrażony jako xiy, co jest dobrym sposobem na przekształcenie go w najbliższy kierunek kompasu?

na przykład

x:+1,  y:+1 => NE
x:0,   y:+3 => N
x:+10, y:-2 => E   // closest compass direction
izb
źródło
chcesz to jako ciąg lub wyliczenie? (tak, to ma znaczenie)
Philipp
Albo, ponieważ będzie używany na dwa sposoby :) Chociaż gdybym musiał wybrać, wziąłbym sznurek.
izb
1
Czy obawiasz się również wykonania, czy tylko zwięzłości?
Marcin Seredynski
2
var angle = Math.atan2 (y, x); return <Direction> Math.floor ((Math.round (angle / (2 * Math.PI / 8))) + 8 + 2)% 8); Używam tego
Kikaimaru
Zwięzły: naznaczony zwięzłością wypowiedzi lub stwierdzeń: wolny od wszelkich opracowań i zbędnych szczegółów. Po prostu to
wyrzucam

Odpowiedzi:

25

Najprostszym sposobem jest prawdopodobnie uzyskanie kąta wektora za pomocą atan2(), jak sugeruje Tetrad w komentarzach, a następnie skalowanie i zaokrąglanie go, np. (Pseudokod):

// 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 ) % 8Linia może wymagać pewnego wyjaśnienia. W całkiem dużo wszystkich języków, które znam, które mają to, funkcja zwraca kąt w radianach. Dzielenie go przez 2 π przekształca go z radianów na ułamki pełnego koła, a pomnożenie przez 8 następnie przekształca go w ósemki koła, które następnie zaokrąglamy do najbliższej liczby całkowitej. Na koniec zmniejszamy go modulo 8, aby zająć się zawijaniem, tak aby zarówno 0, jak i 8 były poprawnie odwzorowane na wschód.atan2()

Powodem tego + 8, który pominąłem powyżej, jest to, że w niektórych językach atan2()mogą zwracać wyniki ujemne (tj. Od - π do + π zamiast od 0 do 2 π ), a operator modulo ( %) może być zdefiniowany tak, aby zwracał wartości ujemne dla argumenty negatywne (lub jego zachowanie w przypadku argumentów negatywnych może być niezdefiniowane). Dodanie 8(tj. Jeden pełny obrót) do danych wejściowych przed redukcją zapewnia, że ​​argumenty są zawsze dodatnie, bez wpływu na wynik w jakikolwiek inny sposób.

Jeśli twój język nie zapewnia wygodnej funkcji zaokrąglania do najbliższego, możesz zamiast tego użyć skracającej konwersji liczb całkowitych i po prostu dodać 0,5 do argumentu, tak jak to:

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

Zauważ, że w niektórych językach domyślna konwersja liczb zmiennoprzecinkowych zaokrągla ujemne wartości wejściowe w górę do zera zamiast w dół, co jest kolejnym powodem, dla którego należy upewnić się, że dane wejściowe są zawsze dodatnie.

Oczywiście możesz zastąpić wszystkie wystąpienia 8tej linii inną liczbą (np. 4 lub 16, a nawet 6 lub 12, jeśli jesteś na mapie heksadecymalnej), aby podzielić okrąg na tyle kierunków. Po prostu dostosuj odpowiednio wyliczanie / tablicę.

Ilmari Karonen
źródło
Zauważ, że zwykle tak atan2(y,x)nie jest atan2(x,y).
sam hocevar
@Sam: Ups, poprawione. Oczywiście atan2(x,y)zadziałałoby również, gdyby po prostu wymienić nagłówki kompasu w kolejności zgodnej z ruchem wskazówek zegara, zaczynając od północy.
Ilmari Karonen
2
Nawiasem mówiąc, +1, naprawdę uważam, że jest to najprostsza i najbardziej rygorystyczna odpowiedź.
sam hocevar
1
@TheLima:octant = round(8 * angle / 360 + 8) % 8
Ilmari Karonen,
1
Należy pamiętać, że cel ten można łatwo przekształcić w 4-way Kompas: quadtant = round(4 * angle / (2*PI) + 4) % 4i przy użyciu ENUM: { E, N, W, S }.
Spoike
10

Masz 8 opcji (lub 16 lub więcej, jeśli chcesz jeszcze dokładniejszej).

enter image description here

Użyj, atan2(y,x)aby uzyskać kąt dla wektora.

atan2() działa w następujący sposób:

enter image description here

Zatem x = 1, y = 0 spowoduje 0, i jest nieciągły przy x = -1, y = 0, zawierający zarówno π, jak i π.

Teraz musimy tylko zmapować wyjście tak, atan2()aby pasowało do kompasu, który mamy powyżej.

Prawdopodobnie najłatwiejszym do wdrożenia jest stopniowe sprawdzanie kątów. Oto pseudo kod, który można łatwo modyfikować w celu zwiększenia precyzji:

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

Teraz, aby dodać większą precyzję, po prostu dodaj wartości do wyliczenia kierunku.

Algorytm działa poprzez sprawdzanie rosnących wartości wokół kompasu, aby sprawdzić, czy nasz kąt leży gdzieś pomiędzy miejscem, w którym ostatnio sprawdzaliśmy, a nową pozycją. Dlatego zaczynamy od -PI + przyrost / 2. Chcemy przesunąć nasze kontrole, aby objąć równą przestrzeń wokół każdego kierunku. Coś takiego:

enter image description here

Zachód jest podzielony na dwie części, ponieważ wartości zwracane atan2()na Zachodzie są nieciągłe.

MichaelHouse
źródło
4
Łatwym sposobem na „przekonwertowanie ich na kąt” jest użycie atan2, choć należy pamiętać, że 0 stopni to prawdopodobnie wschód, a nie północ.
Tetrad
1
Nie potrzebujesz angle >=czeków w powyższym kodzie; na przykład, jeśli kąt jest mniejszy niż 45, wówczas północ zostanie już zwrócona, więc nie trzeba sprawdzać, czy kąt> = 45 dla kontroli wschodniej. Podobnie nie potrzebujesz w ogóle czeku przed powrotem na zachód - to jedyna pozostała możliwość.
MrKWatkins
4
Nie nazwałbym tego zwięzłym sposobem uzyskania wskazówek. Wydaje się dość niezgrabny i będzie wymagał wielu zmian, aby dostosować to do różnych „rozdzielczości”. Nie mówiąc już o tonie ifwypowiedzi, jeśli chcesz iść w 16 lub więcej kierunkach.
bummzack
2
Nie ma potrzeby normalizowania wektora: kąt pozostaje taki sam w przypadku zmian wielkości.
Kylotan
Dzięki @bummzack, zredagowałem post, aby uczynić go bardziej zwięzłym i łatwym do zwiększenia precyzji poprzez dodanie większej liczby wartości wyliczeniowych.
MichaelHouse
8

Ilekroć masz do czynienia z wektorami, rozważ podstawowe operacje wektorowe zamiast konwersji na kąty w określonej ramce.

Biorąc pod uwagę wektor zapytania vi zestaw wektorów jednostkowych s, najbardziej wyrównanym wektorem jest wektor, s_iktóry się maksymalizuje dot(v,s_i). Wynika to z tego, że iloczyn skalarny o ustalonych długościach dla parametrów ma maksimum dla wektorów o tym samym kierunku i minimum dla wektorów o przeciwnych kierunkach, płynnie zmieniających się między nimi.

Uogólnia to ogólnie na więcej niż dwa wymiary, jest rozszerzalne z dowolnymi kierunkami i nie ma problemów specyficznych dla ramki, takich jak nieskończone gradienty.

Pod względem implementacji sprowadzałoby się to do skojarzenia z wektora w każdym kardynalnym kierunku z identyfikatorem (wyliczenie, ciąg, cokolwiek potrzebujesz) reprezentującego ten kierunek. Następnie zapętlisz swój zestaw wskazówek, znajdując ten z najwyższym iloczynem kropkowym.

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];
    }    
}
Lars Viklund
źródło
2
Ta implementacja może być również napisana bez rozgałęzień i wektoryzowana bez większych problemów.
Promit
1
A mapz float2kluczem? To nie wygląda bardzo poważnie.
sam hocevar
Jest to „pseudo-kod” w sposób dydaktyczny. Jeśli chcesz implementacji zoptymalizowanych pod kątem paniki, GDSE prawdopodobnie nie jest dobrym miejscem na twój makaron-makaron. Jeśli chodzi o użycie float2 jako klucza, float może dokładnie reprezentować liczby całkowite, których tu używamy, i możesz zrobić dla nich doskonale dobry komparator. Klucze zmiennoprzecinkowe są nieodpowiednie tylko wtedy, gdy zawierają specjalne wartości lub próbujesz wyszukać obliczone wyniki. Iteracja po sekwencji asocjacyjnej jest w porządku. Jasne, mógłbym użyć liniowego wyszukiwania w tablicy, ale byłby to bezcelowy bałagan.
Lars Viklund
3

Jednym ze sposobów, o którym nie wspomniano tutaj, jest traktowanie wektorów jako liczb zespolonych. Nie wymagają trygonometrii i mogą być dość intuicyjne w dodawaniu, mnożeniu lub zaokrąglaniu rotacji, zwłaszcza, że ​​nagłówki są już reprezentowane jako pary liczb.

W przypadku, gdy nie jesteś ich zaznajomiony, kierunki są wyrażone w postaci a + b (i), a istota jest składnikiem rzeczywistym, a b (i) jest urojoną. Jeśli wyobrażasz sobie płaszczyznę kartezjańską z X będącym rzeczywistym, a Y będącym urojonym, 1 byłby na wschodzie (po prawej), byłbym na północy.

Oto kluczowa część: 8 głównych kierunków jest reprezentowanych wyłącznie przez liczby 1, -1 lub 0 dla ich rzeczywistych i urojonych składników. Wszystko, co musisz zrobić, to zmniejszyć współrzędne X, Y jako stosunek i zaokrąglić obie do najbliższej liczby całkowitej, aby uzyskać kierunek.

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

W celu zamiany kierunku na najbliższą przekątną zmniejsz proporcjonalnie zarówno X, jak i Y, aby większa wartość wynosiła dokładnie 1 lub -1. Zestaw

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

Zaokrąglenie obu składników pierwotnie (10, -2) daje 1 + 0 (i) lub 1. Więc najbliższy kierunek to wschód.

Powyższe w rzeczywistości nie wymaga użycia złożonej struktury liczbowej, ale myślenie o nich jako takich przyspiesza znalezienie 8 głównych kierunków. Możesz zrobić matematykę wektorową w zwykły sposób, jeśli chcesz uzyskać nagłówek netto dwóch lub więcej wektorów. (Jako liczby zespolone nie dodajesz, ale mnożysz dla wyniku)

ChrisC
źródło
1
To niesamowite, ale popełnia błąd podobny do tego, który popełniłem przy własnej próbie. Odpowiedzi są bliskie, ale nieprawidłowe. Kąt graniczny między E i NE wynosi 22,5 stopnia, ale odcina się przy 26,6 stopni.
izb
Max(x, y)powinien Max(Abs(x, y))pracować dla ujemnych kwadrantów. Próbowałem i uzyskałem ten sam wynik co izb - to zmienia kierunek kompasu pod niewłaściwym kątem. Sądzę, że zmieniłby się, gdy kurs. Y / kurs. X przekroczy 0,5 (więc zaokrąglona wartość zmienia się z 0 na 1), co oznacza arctan (0,5) = 26,565 °.
amitp
Innym sposobem użycia liczb zespolonych w tym przypadku jest zaobserwowanie, że mnożenie liczb zespolonych wymaga rotacji. Jeśli konstruujesz liczbę zespoloną reprezentującą 1/8 obrotu wokół koła, to za każdym razem, gdy ją pomnożysz, przesuwasz jedną oktanę. Więc możesz zapytać: czy możemy policzyć, ile mnożników zajęło przejście ze Wschodu do obecnego kursu? Odpowiedź na „ile razy musimy przez to pomnożyć” to: logarytm . Jeśli szukasz logarytmów dla liczb zespolonych… używa atan2. Ostatecznie jest to równoważne z odpowiedzią Ilmari.
amitp
-2

to wydaje się działać:

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"};
}
Ray Tayek
źródło
dlaczego jest to przegłosowane?
Ray Tayek
Najprawdopodobniej dlatego, że nie ma wyjaśnienia twojego kodu. Dlaczego to jest rozwiązanie i jak działa?
Vaillancourt
prowadziłeś to?
Ray Tayek
Nie, a biorąc pod uwagę nazwę klasy, zakładałem, że to zrobiłeś i zadziałało. I to świetnie. Ale zapytałeś, dlaczego ludzie głosowali, a ja odpowiedziałem; Nigdy nie sugerowałem, że to nie zadziałało :)
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 + znak (x)) * (1-znak (y ^ 2)) - (2 + znak (x)) * znak (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)
theodore panagos
źródło
Na razie jest to tylko garść postaci, które nie mają większego sensu; dlaczego jest to rozwiązanie, które zadziałałoby na pytanie, jak to działa?
Vaillancourt
Piszę formułę tak, jak napisałem jn excel i działa idealnie.
theodore panagos
= MOD ((4-2 * (1 + ZNAK (X1)) * (1-ZNAK (Y1 ^ 2)) - (2 + ZNAK (X1)) * ZNAK (Y1) - (1 + ZNAK (ABS (ZNAK) (X1 * Y1) * ATAN ((ABS (X1) -ABS (Y1)) / (ABS (X1) + ABS (Y1)))) - PI () / (8 + 10 ^ -15))) / 2 * ZNAK ((X1 ^ 2-Y1 ^ 2) * (X1 * Y1))), 8)
theodore panagos 24.01.2019
-4

Kiedy chcesz ciąg:

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)

Daje to stałe poprzez wykorzystanie pól bitowych:

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

Nieznaczną poprawą wydajności byłoby umieszczenie <-checks w gałęzi else odpowiednich >-checks, ale powstrzymałem się od robienia tego, ponieważ szkodzi to czytelności.

Philipp
źródło
2
Przepraszam, ale to nie da dokładnie odpowiedzi, której szukam. Dzięki temu kodowi będzie dawać „N”, jeśli wektor jest dokładnie na północ, a NE lub NW, jeśli x jest dowolną inną wartością. Potrzebuję najbliższego kierunku kompasu, np. Jeśli wektor jest bliżej N niż NW, wówczas zwróci N.
izb
Czy to rzeczywiście dałoby najbliższy kierunek? Wygląda na to, że wektor (0,00001,100) dałby ci północny wschód. edytuj: pobiłeś mnie do tego izb.
CiscoIPPhone
nie powiedziałeś, że chcesz najbliższego kierunku.
Philipp
1
Przepraszam, ukryłem to w tytule. Powinno być jaśniej w
tekście
1
A co z używaniem nieskończonej normy? Dzielenie przez max (abs (vector.components)) daje znormalizowany wektor w odniesieniu do tej normy. Teraz możesz napisać małą tabelę kontrolną na podstawie if (x > 0.9) dir |= DIR_Ecałej reszty. Powinien być lepszy niż oryginalny kod Phillippa i nieco tańszy niż stosowanie norm L2 i atan2. Może, a może nie.
teodron