Bir 2D vektörünü en yakın 8 yollu pusula yönüne dönüştürmenin en iyi yolu nedir?


17

Eğer x ve y olarak ifade edilen bir 2B vektörünüz varsa, bunu en yakın pusula yönüne dönüştürmenin iyi bir yolu nedir?

Örneğin

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

onu bir dize veya bir numaralandırma olarak istiyor musunuz? (evet, önemli)
Philipp

Ya, her iki şekilde de kullanılacağından :) Seçmem gerekirse, bir ip alırdım.
izb

1
Performanstan da mı, yoksa yalnızca özlülükten mi endişe ediyorsunuz?
Marcin Seredynski

2
var angle = Math.atan2 (y, x); dönüş <Yön> Math.floor ((Math.round (açı / (2 * Math.PI / 8)) + 8 + 2)% 8); Bunu kullanıyorum
Kikaimaru

Özlü: ifade veya ifade özüyle işaretlenir: tüm ayrıntılardan ve gereksiz detaylardan arınmış. Sadece ... Oradaki dışarı atma
Dialock

Yanıtlar:


25

En basit yol, atan2()Tetrad'ın yorumlarda önerdiği gibi , muhtemelen vektör açısını elde etmek ve ardından ölçeklendirmek ve yuvarlamaktır, örneğin (sözde kod):

// 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 ) % 8Hat bir açıklama gerekebilir. Bildiğim hemen hemen tüm dillerde , atan2()fonksiyon radyan cinsinden açıyı döndürür. Bölme 2 π , radyandan tam bir dairenin fraksiyonlarına dönüştürür ve 8 ile çarpmak, daha sonra en yakın tamsayıya yuvarladığımız bir dairenin sekizinci haline dönüştürür. Son olarak, etrafı sarmak için modulo 8'i azaltıyoruz, böylece hem 0 hem de 8 doğru bir şekilde doğu ile eşleştiriliyor.

+ 8Yukarıda atladığımın nedeni , bazı dillerde atan2()negatif sonuçlar ( 0'dan 2 π yerine - π ila + π arasında ) döndürebilmesi ve modulo operatörünün ( ) negatif argümanlar (veya negatif argümanlar için davranışı tanımlanmamış olabilir). Ekleme%8İndirgeme işleminden önce girdiye (bir tam dönüş) , bağımsız değişkenlerin sonucu başka hiçbir şekilde etkilemeden her zaman pozitif olmasını sağlar.

Diliniz uygun bir en yakın yuvarlama işlevi sağlamazsa, bunun yerine kısaltılmış bir tamsayı dönüşümü kullanabilir ve bağımsız değişkene şu şekilde 0,5 ekleyebilirsiniz:

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

Bazı dillerde, varsayılan kayan noktalı sayıdan tamsayıya dönüşümün negatif girdileri aşağıdan ziyade sıfıra yuvarladığını unutmayın; bu da girdinin her zaman pozitif olmasını sağlamak için başka bir nedendir.

Tabii ki, 8daireyi bu kadar yöne bölmek için o satırdaki tüm oluşumları başka bir numarayla (örneğin, 4 veya 16, hatta altıgen bir haritadaysanız 6 veya 12) değiştirebilirsiniz. Numarayı / diziyi buna göre ayarlamanız yeterlidir.


Bunun genellikle atan2(y,x)olmadığını unutmayın atan2(x,y).
sam hocevar

@ Sam: Hata! Düzeltildi. Tabii ki, atan2(x,y)pusula başlıklarını kuzeyden başlayarak saat yönünde sıraladıysa da işe yarar.
Ilmari Karonen

2
Bu arada +1, bunun en basit ve titiz cevap olduğunu düşünüyorum.
sam hocevar

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

1
Bunun kolayca 4-yollu bir pusulaya dönüştürülebileceğini quadtant = round(4 * angle / (2*PI) + 4) % 4ve enum: 'u kullanabileceğini unutmayın { E, N, W, S }.
22'de Spoike

10

8 seçeneğiniz var (veya daha hassas bir hassasiyet istiyorsanız 16 veya daha fazla).

enter image description here

kullanım atan2(y,x)Vektörünüz için açı elde etmek için .

atan2() aşağıdaki şekilde çalışır:

enter image description here

Yani x = 1, y = 0, 0 ile sonuçlanır ve x = -1, y = 0'da hem π hem de -π içeren süreksizdir.

Şimdi sadece atan2() yukarıdaki pusulanın .

Uygulanması en kolay olanı, açıların artan bir kontrolüdür. Yüksek hassasiyet için kolayca değiştirilebilen bazı sahte kodlar:

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

Şimdi daha fazla hassasiyet eklemek için değerleri yön numarasına eklemeniz yeterlidir.

Algoritma, açımızın en son kontrol ettiğimiz yer ile yeni konum arasında bir yerde olup olmadığını görmek için pusula çevresindeki artan değerleri kontrol ederek çalışır. Bu yüzden -PI + artışıyla başlıyoruz / 2. Çeklerimizi her yönde eşit alan içerecek şekilde dengelemek istiyoruz. Bunun gibi bir şey:

enter image description here

Batı ikiye bölündü çünkü atan2()Batı'daki dönüş değerleri süreksiz.


4
"Onları bir açıya dönüştürmenin" kolay bir yolu kullanmaktır atan2, ancak 0 derecenin muhtemelen kuzey değil, doğu olacağını unutmayın.
Tetrad

1
angle >=Yukarıdaki koddaki kontrollere ihtiyacınız yoktur ; örneğin açı 45'ten küçükse, kuzey zaten döndürülmüş olacaktır, bu nedenle doğu kontrolü için açının> = 45 olup olmadığını kontrol etmenize gerek yoktur. Benzer şekilde, batıya dönmeden önce herhangi bir kontrole ihtiyacınız yoktur - kalan tek olasılık budur.
MrKWatkins

4
Buna yön bulmanın kısa bir yolu demem. Oldukça karmaşık görünüyor ve bunu farklı "çözümlere" uyarlamak için birçok değişiklik gerektirecek. if16 veya daha fazla yön için gitmek istiyorsanız , bir ton ifadeden bahsetmiyorum .
bummzack

2
Vektörü normalleştirmeye gerek yok: açı, büyüklük değişimlerinde aynı kalır.
Kylotan

Teşekkürler @bummzack, yalnızca daha fazla numara değeri ekleyerek hassasiyeti artırmayı daha kısa ve kolay hale getirmek için yayını düzenledim.
MichaelHouse

8

Vektörlerle uğraşırken, belirli bir çerçevedeki açılara dönüştürmek yerine temel vektör işlemlerini düşünün.

Bir sorgu vektörü vve bir dizi birim vektör sverildiğinde, en hizalanmış vektör, s_imaksimize edilen vektördür dot(v,s_i). Bunun nedeni, parametreler için sabit uzunluklar verilen nokta ürünün, aynı yöne sahip vektörler için maksimum ve karşıt yönlere sahip vektörler için minimum, aralarında sorunsuz bir şekilde değişmesidir.

Bu önemsiz olarak ikiden daha fazla boyuta genelleme yapar, keyfi yönlerle genişletilebilir ve sonsuz degradeler gibi çerçeveye özgü sorunlara maruz kalmaz.

Uygulama açısından, bu, her bir kardinal yönde bir vektörden, o yönü temsil eden bir tanımlayıcıyla (enum, dize, ihtiyacınız olan her şey) ilişkilendirmeye kadar kaynar. Daha sonra, en yüksek nokta ürününe sahip olanı bularak yol tarifleri setinizin üzerinden geçersiniz.

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
Bu uygulama aynı zamanda dalsız olarak yazılabilir ve çok fazla sorun olmadan vektörleştirilebilir.
ProMit

1
A mapile float2anahtar? Bu çok ciddi görünmüyor.
sam hocevar

Didaktik bir şekilde "sahte kod" dur. Panik için optimize edilmiş uygulamalar istiyorsanız, GDSE muhtemelen makarnalarınız için gidilecek yer değil. Float2'yi anahtar olarak kullanmaya gelince, bir float burada kullandığımız tüm sayıları tam olarak temsil edebilir ve onlar için mükemmel bir karşılaştırıcı yapabilirsiniz. Kayan nokta tuşları yalnızca özel değerler içeriyorsa veya hesaplanan sonuçları aramaya çalışırsanız uygun değildir. İlişkisel bir dizi üzerinden yineleme iyidir. Bir dizide doğrusal bir arama kullanabilirdim, elbette, ama anlamsız bir karmaşa olurdu.
Lars Viklund

3

Burada bahsedilmeyen bir yol, vektörlere karmaşık sayılar olarak muamele etmektir. Trigonometri gerektirmezler ve özellikle rotasyonlarınızı eklemek, çoğaltmak veya yuvarlamak için oldukça sezgisel olabilirler, çünkü özellikle başlıklarınızı sayı çiftleri olarak temsil ediyorsunuz.

Onlara aşina değilseniz, yönler gerçek bileşen olmakla birlikte a + b (i) şeklinde ifade edilir ve b (i) hayali olur. X'in gerçek ve Y'nin hayali olduğu kartezyen uçağı hayal ederseniz, 1 doğu olur (sağda), ben kuzey olurum.

Burada kilit kısım: 8 temel yön, gerçek ve hayali bileşenleri için sadece 1, -1 veya 0 rakamlarıyla temsil edilir. Tek yapmanız gereken X, Y koordinatlarınızı bir oran olarak azaltmak ve yönü elde etmek için her ikisini de en yakın tam sayıya yuvarlamaktır.

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

Başlık-en yakın çapraz dönüşüm için, hem X hem de Y'yi orantılı olarak azaltın, böylece daha büyük değer tam olarak 1 veya -1 olur. Ayarlamak

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

Orijinal olanın (10, -2) her iki bileşeninin yuvarlanması size 1 + 0 (i) veya 1 verir. Böylece en yakın yön doğudır.

Yukarıdakiler aslında karmaşık bir sayı yapısının kullanılmasını gerektirmez, ancak bunları böyle düşünmek 8 kardinal yönü bulmayı daha hızlı hale getirir. İki veya daha fazla vektörün net başlığını almak istiyorsanız, vektör matematiğini normal şekilde yapabilirsiniz. (Karmaşık sayılar olarak, eklemezsiniz, ancak sonuç için çarparsınız)


1
Bu harika, ama kendi girişimimde yaptığım hataya benzer bir hata yapıyor. Cevaplar yakın ama doğru değil. E ve NE arasındaki sınır açısı 22.5 derecedir, ancak bu 26.6 derecede kesilir.
izb

Max(x, y)Max(Abs(x, y))negatif çeyrekler için çalışmak olmalı . Denedim ve izb ile aynı sonucu aldım - bu pusula yönlerini yanlış açılarda değiştirir. Ben heading.y / heading.x 0,5 (yani yuvarlatılmış değer 0'dan 1'e geçer), arctan (0,5) = 26,565 ° olduğunda geçiş olacağını tahmin ediyorum.
amitp

Burada karmaşık sayıları kullanmanın farklı bir yolu, karmaşık sayıların çarpımının bir dönüş içerdiğini gözlemlemektir. Bir daire etrafındaki dönüşün 1 / 8'ini temsil eden karmaşık bir sayı oluşturursanız, o zaman onunla her çarptığınızda bir oktanı taşırsınız. Yani sorabilirsiniz: Doğu'dan şu anki istikamete gitmek için kaç çarpma işlemi yaptığını sayabilir miyiz? "Kaç kere bununla çarpmamız gerekiyor" cevabı bir logaritmadır . Karmaşık sayılar için logaritmalara bakarsanız… atan2'yi kullanır. Böylece bu Ilmari'nin cevabına eşdeğer olur.
amitp

-2

Bu işe yarıyor gibi görünüyor:

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

bu neden oylanıyor?
Ray Tayek

Büyük olasılıkla kodunuzun arkasında bir açıklama olmadığı için. Bu neden çözüm ve nasıl çalışıyor?
Vaillancourt

koştun mu
Ray Tayek

Hayır ve sınıfın adı verildiğinde, yaptığınızı ve işe yaradığını varsaydım. Ve bu harika. Ama siz insanların neden oy kullandığını sordunuz ve ben cevapladım; Ben işe yaramadı ima asla :)
Vaillancourt

-2

E = 0, NS = 1, n = 2, K = 3, B = 4, GB = 5, S = 6, SH = 7

f (x, y) = mod ((4-2 * (1 + işareti (x)) * (1-işareti (y ^ 2)) - (2 + işareti (x)) * işareti (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)

Şimdilik, bu pek mantıklı olmayan bir grup karakter; neden bu soru için işe yarayacak bir çözüm, nasıl çalışır?
Vaillancourt

Formülü jn excel yazarken yazıyorum ve mükemmel çalışıyor.
theodore panagos

= MOD ((4-2 * (1 + İŞARET (X1)) * (1-İŞARET (Y1 ^ 2)) - (2 + İŞARET (X1)) * İŞARET (Y1) - (1 + İŞARET (ABS (İŞARET) (X1 * Y1) * ATAN ((ABS (X1) -ABS (Y1)) / (ABS (X1) + ABS (Y1)))) - PI () / (8 + 10 ^ -15))) / 2 * İŞARET ((X1 ^ 2-Y1 ^ 2) * (X1 * Y1))), 8)
theodore panagos

-4

Bir dize istediğinizde:

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)

Bu, bit alanlarını kullanarak size sabitler verir:

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

Hafif bir performans artışı <-Kontrolleri karşılık gelen >-kontrollerin diğer-şubesine , ancak bunu yapmaktan kaçındım çünkü okunabilirliğe zarar veriyor.


2
Üzgünüm, ama bu tam olarak aradığım cevabı vermeyecek. Bu kod ile sadece vektör tam olarak kuzey ise "N" ve x başka bir değerse NE veya NW verir. İhtiyacım olan en yakın pusula yönü, örneğin vektör
NW'den

Bu gerçekten en yakın yönü verir mi? Görünüşe göre (0.00001,100) bir vektör size kuzey doğu verir. düzenleme: sen beni dövdü izb.
CiscoIPPhone

en yakın yönü istediğini söylemedin.
Philipp

1
Üzgünüm, bunu başlıkta sakladım. Soru organında daha açık
olmalıydı

1
Sonsuz normu kullanmaya ne dersiniz? Max'e (abs (vektör. Bileşenler)) bölmek size bu norm açısından normalleştirilmiş bir vektör verir. Artık küçük bir check-up masası yazabiliyorsunuz if (x > 0.9) dir |= DIR_E. Phillipp'in orijinal kodundan daha iyi olmalı ve L2 normu ve atan2'yi kullanmaktan biraz daha ucuz olmalıdır. Belki .. ya da olmayabilir.
teodron
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.