Каков наилучший способ преобразования 2D-вектора в ближайшее направление компаса?

17

Если у вас есть двумерный вектор, выраженный как x и y, каков хороший способ преобразовать это в ближайшее направление компаса?

например

x:+1,  y:+1 => NE
x:0,   y:+3 => N
x:+10, y:-2 => E   // closest compass direction
IZB
источник
ты хочешь это как строка или перечисление? (да, это важно)
Филипп
Либо, так как он будет использоваться обоими способами :) Хотя, если бы мне пришлось выбирать, я бы взял строку.
IZB
1
Вы также обеспокоены производительностью или только краткостью?
Марчин Серединский
2
угол угла = Math.atan2 (y, x); return <Direction> Math.floor ((Math.round (angle / (2 * Math.PI / 8)) + 8 + 2)% 8); Я использую это
Kikaimaru
Краткий: отмечен краткостью выражения или высказывания: свободен от всех разработок и лишних деталей. Просто выбросить это там ...
Dialock

Ответы:

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 π ), и оператор modulo ( %) может быть определен так, чтобы возвращать отрицательные значения для отрицательные аргументы (или его поведение для отрицательных аргументов может быть неопределенным). Добавление 8(т. Е. Один полный оборот) к входу до сокращения гарантирует, что аргументы всегда положительны, не влияя на результат каким-либо другим образом.

Если ваш язык не предоставляет удобную функцию округления до ближайшего, вы можете вместо этого использовать усеченное целочисленное преобразование и просто добавить 0,5 к аргументу, например так:

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

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

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

Илмари Каронен
источник
Обратите внимание, что обычно это atan2(y,x)не так atan2(x,y).
Сэм Хоцевар
@Sam: К сожалению, исправлено. Конечно, atan2(x,y)это также сработало бы, если бы кто-то просто перечислял заголовки компаса по часовой стрелке, начиная с севера.
Илмари Каронен
2
+1 кстати, я действительно считаю, что это самый простой и сложный ответ.
Сэм Хоцевар
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 или более, если вы хотите еще более высокую точность).

введите описание изображения здесь

Используйте, atan2(y,x)чтобы получить угол для вашего вектора.

atan2() работает следующим образом:

введите описание изображения здесь

Таким образом, 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]

Теперь, чтобы добавить больше точности, просто добавьте значения в перечисление direction.

Алгоритм работает, проверяя возрастающие значения вокруг компаса, чтобы увидеть, лежит ли наш угол где-то между тем, где мы в последний раз проверяли, и новой позицией. Вот почему мы начинаем с -PI + increment / 2. Мы хотим компенсировать наши проверки, чтобы включить одинаковое пространство вокруг каждого направления. Что-то вроде этого:

введите описание изображения здесь

Запад разбит на две части, потому что возвращаемые значения atan2()на Западе носят прерывистый характер.

MichaelHouse
источник
4
Простой способ «преобразовать их в угол» - использовать atan2, хотя имейте в виду, что 0 градусов, вероятно, будут восточными, а не северными.
Тетрад
1
Вам не нужны angle >=проверки в коде выше; например, если угол меньше 45, то север уже будет возвращен, поэтому вам не нужно проверять угол> = 45 для проверки на восток. Точно так же перед возвращением на запад вам вообще не нужна проверка - это единственная оставшаяся возможность.
MrKWatkins
4
Я бы не назвал это кратким способом определения направления. Это кажется довольно неуклюжим и потребует много изменений, чтобы приспособить это к различным «разрешениям». Не говоря уже о куче ifутверждений, если вы хотите пойти по 16 или более направлениям.
bummzack
2
Нет необходимости нормализовать вектор: угол остается неизменным при изменении величины.
Килотан
Спасибо @bummzack, я отредактировал пост, чтобы сделать его более кратким и простым для повышения точности, просто добавив больше значений enum.
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
Эта реализация также может быть написана без ветвления и векторизована без особых проблем.
Promit
1
А mapс float2ключом? Это не выглядит очень серьезно.
Сэм Хоцевар
Это «псевдокод» в дидактической манере. Если вам нужны реализации, оптимизированные для паники, скорее всего, GDSE - это не то место, куда вы можете отправиться за своей копировальной пастой. Что касается использования float2 в качестве ключа, float может точно представлять целые числа, которые мы здесь используем, и вы можете сделать для них отличный компаратор. Ключи с плавающей запятой не подходят, только если они содержат специальные значения или вы пытаетесь найти вычисленные результаты. Итерация по ассоциативной последовательности - это хорошо. Конечно, я мог бы использовать линейный поиск в массиве, но это было бы бесполезным беспорядком.
Ларс Виклунд
3

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

В случае, если вы не знакомы с ними, направления выражаются в виде a + 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 основных направлений. Вы можете сделать векторную математику обычным способом, если хотите получить чистый заголовок из двух или более векторов. (Как комплексные числа, вы не добавляете, а умножаете для результата)

ChrisC
источник
1
Это потрясающе, но делает ошибку, аналогичную той, которую я сделал в своей собственной попытке. Ответы близки, но не верны. Граничный угол между E и NE составляет 22,5 градуса, но он составляет 26,6 градуса.
IZB
Max(x, y)должен Max(Abs(x, y))работать на отрицательные квадранты. Я попробовал это и получил тот же результат, что и izb - это переключает направления компаса под неправильными углами. Я предполагаю, что он будет переключаться, когда heading.y / heading.x пересекает 0,5 (поэтому округленное значение переключается с 0 на 1), что составляет arctan (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

Е = 0, СВ = 1, N = 2, СЗ = 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)))) - ПИ () / (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

Небольшое улучшение производительности может заключаться в том, чтобы поместить <-checks в ветку else соответствующих >-checks, но я воздержался от этого, потому что это вредит читабельности.

Philipp
источник
2
Извините, но это не даст именно тот ответ, который я ищу. С этим кодом он выдаст «N», только если вектор расположен точно на север, а NE или NW, если x - любое другое значение. То, что мне нужно, это ближайшее направление компаса, например, если вектор ближе к N, чем к северо-западу, тогда он даст N.
izb
Это действительно дало бы самое близкое направление? Кажется, вектор (0,00001,100) даст вам северо-восток. редактировать: вы избили меня к этому изб.
CiscoIPPhone
Вы не сказали, что хотите самое близкое направление.
Филипп
1
Извините, я спрятал это в заголовке. Если бы яснее в теле вопроса
IZB
1
Как насчет использования бесконечной нормы? Деление на max (abs (vector.components)) дает нормализованный вектор по отношению к этой норме. Теперь вы можете написать небольшую контрольную таблицу на основе if (x > 0.9) dir |= DIR_Eвсего остального. Это должно быть лучше, чем оригинальный код Филиппа, и немного дешевле, чем использование нормы L2 и atan2. Может быть, а может и нет.
Теодрон