Який найкращий спосіб перетворення 2D вектора в найближчий восьмисторонній компас?


17

Якщо у вас двовимірний вектор, виражений x і y, який хороший спосіб перетворити його в найближчий напрямок компаса?

напр

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); повернути <Направлення> Math.floor ((Math.round (кут / (2 * Math.PI / 8)) + 8 + 2)% 8); Я використовую цей
Kikaimaru

Короткий: позначений стислістю висловлювання або висловлювання: без будь-якої розробки та зайвих деталей. Просто викинувши це там ...
Діалок

Відповіді:


25

Найпростіший спосіб - це, мабуть, отримати кут вектора за допомогою atan2(), як пропонує тетрад у коментарях, а потім масштабувати і округляти його, наприклад (псевдокод):

// 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Лінії , можливо , буде потрібно якийсь - то пояснення. У майже всіх мовах , які я знаю , що у нього, функція повертає кут в радіанах. Ділення на 2 π перетворює його від радіану на дроби повного кола, а помноживши на 8, перетворює його на вісімку окружності, яку потім округлимо до найближчого цілого числа. Нарешті, ми зменшуємо його по модулю 8, щоб дбати про обертання, щоб і 0, і 8 було правильно відображено на схід.atan2()

Причина + 8, яку я пропустив раніше, полягає в тому, що в деяких мовах atan2()можуть повертатися негативні результати (тобто від - π до + π, а не від 0 до 2 π ), і оператор модуля ( %) може бути визначений для повернення від'ємних значень для негативні аргументи (або його поведінка щодо негативних аргументів може бути невизначеною). Додавання 8(тобто один повний поворот) до вводу перед скороченням гарантує, що аргументи завжди позитивні, не впливаючи на результат будь-яким іншим способом.

Якщо у вашій мові не передбачено зручної функції "круглого до найближчого", ви можете замість цього перетворити цілочисельне перетворення і просто додати 0,5 до аргументу, як це:

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

Зауважте, що в деяких мовах конверсія з плаваючою цілою на цілі цілі обводить негативні входи в бік до нуля, а не вниз, що є ще однією причиною впевненості, що введення завжди позитивне.

Звичайно, ви можете замінити всі входження 8на цьому рядку на якесь інше число (наприклад, 4 або 16, або навіть 6 або 12, якщо ви знаходитесь на шістнадцятковій карті), щоб розділити коло на стільки напрямків. Просто відповідно відрегулюйте enum / масив.


Зауважте, що це зазвичай atan2(y,x), ні atan2(x,y).
sam hocevar

@Sam: На жаль, виправлено. Зрозуміло, atan2(x,y)це також спрацювало, якби тільки що перераховано заголовки компасів у порядку годинникової стрілки, починаючи з півночі.
Ільмарі Каронен

2
+1, до речі, я дійсно вважаю, що це найпростіша і сувора відповідь.
sam hocevar

1
@TheLima:octant = round(8 * angle / 360 + 8) % 8
Ільмарі Каронен

1
Зверніть увагу , що це може бути легко перетворений в 4-ходовим компас: quadtant = round(4 * angle / (2*PI) + 4) % 4і з допомогою перерахування: { E, N, W, S }.
Спіке

10

У вас є 8 варіантів (або 16 і більше, якщо ви хочете ще більш точної).

enter image description here

Використовуйте atan2(y,x)для отримання кута для вашого вектора.

atan2() працює наступним чином:

enter image description here

Тож 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 + приріст / 2. Ми хочемо зрушити наші чеки, щоб вони містили рівний простір навколо кожного напрямку. Щось на зразок цього:

enter image description here

Захід розбивається надвоє, оскільки значення повернення atan2()на West припиняються.


4
Простий спосіб "перетворити їх на кут" - це використовувати atan2, хоча майте на увазі, що 0 градусів, ймовірно, буде на схід, а не на північ.
Тетрад

1
Вам не потрібні angle >=чеки в наведеному вище коді; наприклад, якщо кут менше 45, то північ вже буде повернуто, тому вам не потрібно перевіряти, чи кут> = 45 для східної перевірки. Так само вам взагалі не потрібна перевірка перед поверненням на захід - це єдина можливість.
MrKWatkins

4
Я б не назвав це стислим способом отримати напрямок. Це здається досить незграбним і потребує численних змін, щоб адаптувати це до різних "резолюцій". Не кажучи про тону ifтверджень, якщо ви хочете пройти 16 напрямків і більше.
bummzack

2
Не потрібно нормалізувати вектор: кут залишається однаковим при зміні величини.
Kylotan

Дякую @bummzack, я відредагував публікацію, щоб зробити її більш короткою та простою, щоб підвищити точність, просто додаючи більше значень перерахувань.
MichaelHouse

8

Щоразу, коли ви маєте справу з векторами, враховуйте фундаментальні векторні операції, а не перетворення кутів у певному кадрі.

Враховуючи вектор запиту vта набір одиничних векторів s, найбільш вирівняним вектором є вектор, s_iякий максимізується dot(v,s_i). Це пов’язано з тим, що крапковий добуток із заданими фіксованими довжинами для параметрів має максимум для векторів з однаковим напрямком і мінімум для векторів з протилежними напрямками, плавно змінюючись між ними.

Це узагальнюється тривіально на більше розмірів, ніж два, розширюється довільними напрямками і не зазнає конкретних кадрів проблем, як нескінченний градієнт.

Згідно з реалізацією, це зводиться до асоціації вектора у кожному кардинальному напрямку з ідентифікатором (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
Ця реалізація також може бути написана без гілок і векторизована без особливих проблем.
Обіцяйте

1
А mapз float2ключем? Це не дуже серйозно.
sam hocevar

Це "псевдокод" у дидактичній формі. Якщо ви хочете оптимізованих реалізацій, GDSE, швидше за все, не буде місцем для вашої копії пасти. Що стосується використання float2 в якості ключа, float може точно представляти цілі числа, які ми тут використовуємо, і ви можете зробити для них абсолютно хороший компаратор. Клавіші з плаваючою комою непридатні лише у тому випадку, якщо вони містять спеціальні значення або якщо ви намагаєтеся шукати обчислені результати. Ітерація над асоціативною послідовністю - це добре. Я міг би використовувати лінійний пошук у масиві, звичайно, але це було б просто безглуздо.
Ларс Віклунд

3

Один із способів, про який тут не згадувалося - трактування векторів як складних чисел. Вони не потребують тригонометрії і можуть бути досить інтуїтивно зрозумілими для додавання, множення чи округлення обертів, тим більше, що у вас уже заголовки представлені у вигляді пари чисел.

Якщо ви не знайомі з ними, вказівки виражаються у вигляді + b (i) з істотою, що є реальною складовою, а b (i) - уявною. Якщо ви уявляєте декартову площину, коли X справжній, а Y уявний, 1 буде схід (праворуч), я був би північ.

Ось ключова частина: 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 головних напрямків. Ви можете займатися векторною математикою звичайним способом, якщо хочете отримати чистий заголовок двох або більше векторів. (Як складні числа, ви не додаєте, а множите для результату)


1
Це дивовижно, але робить аналогічну помилку з тією, яку я зробив у власній спробі. Відповіді близькі, але неправильні. Граничний кут між E і NE становить 22,5 градусів, але це відрізається в 26,6 градусів.
ізб

Max(x, y)повинно Max(Abs(x, y))працювати над негативними квадрантами. Я спробував це і отримав той же результат, що і izb - це перемикає напрямки компаса під неправильними кутами. Я б припустив, що він переключиться, коли заголовок.y / header.x перетинає 0,5 (тому округлене значення переходить з 0 на 1), що є арктаном (0,5) = 26,565 °.
amitp

Інший спосіб використання складних чисел тут - спостерігати, що множення складних чисел передбачає обертання. Якщо ви побудуєте складне число, яке представляє 1/8 обертання навколо кола, то кожного разу, коли ви помножите на нього, ви переміщуєте одного октанта. Тож ви можете запитати: чи можемо ми порахувати, скільки множин знадобилось, щоб перейти зі Сходу до поточного заголовка? Відповідь "скільки разів нам доводиться множити на це" - це логарифм . Якщо ви шукаєте логарифми для складних чисел ... він використовує atan2. Отже, це в кінцевому підсумку є рівнозначним відповіді Ільмарі.
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

ти це запустив?
Рей Тайек

Ні, і, даючи назву класу, я припускав, що ви це зробили, і він спрацював. І це чудово. Але ви запитали, чому люди проголосували, і я відповів; Я ніколи не мав на увазі, що це не працює :)
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 + знак (x)) * (1-знак (y ^ 2)) - (2 + знак (x)) * знак (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)

Наразі це лише купа персонажів, які не мають особливого сенсу; чому це рішення, яке б спрацювало на питання, як воно працює?
Vaillancourt

Я формулю формулу, як я написав 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 * ПІДПИС ((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

Невелике поліпшення продуктивності полягатиме в тому, щоб помістити <-чеки в іншу гілку відповідних >-проверок, але я утримався від цього, оскільки це шкодить читабельності.


2
Вибачте, але це не дасть точно відповіді, яку я шукаю. З цим кодом він отримає "N" лише тоді, коли вектор точно північніший, а NE або NW, якщо x - будь-яке інше значення. Що мені потрібно, це найближчий напрямок компаса, наприклад, якщо вектор ближче до N, ніж NW, він дасть N.
izb

Чи насправді це дасть найближчий напрямок? Здається, вектор (0,00001,100) дав би вам північний схід. редагувати: ти мене бив до цього ізб.
CiscoIPPhone

ви не сказали, що хочете найближчого напрямку.
Філіп

1
Вибачте, я заховав це в назві. Повинно було бути зрозумілішим у запитанні
ізб

1
Що з використанням нескінченної норми? Ділення на max (abs (vector.components)) дає вам нормалізований вектор щодо цієї норми. Тепер ви можете написати невелику контрольну таблицю на основі if (x > 0.9) dir |= DIR_Eта все інше. Це повинно бути краще, ніж оригінальний код Філіппа, і трохи дешевше, ніж використання норми L2 та atan2. Можливо .. а може й ні.
теодрон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.