Qual é a melhor maneira de transformar um vetor 2D na direção da bússola de 8 direções mais próxima?


17

Se você tem um vetor 2D expresso em xey, qual é a melhor maneira de transformá-lo na direção da bússola mais próxima?

por exemplo

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

você quer isso como uma string ou um enum? (sim, isso importa)
Philipp

Ou, uma vez que será usado nos dois sentidos :) Embora se eu tivesse que escolher, eu pegaria uma corda.
izb

1
Você também está preocupado com o desempenho ou apenas com concisão?
Marcin Seredynski

2
ângulo de var = Math.atan2 (y, x); return <Direction> Math.floor ((Math.round (ângulo / (2 * Math.PI / 8)) + 8 + 2)% 8); Eu uso este aqui
Kikaimaru

Conciso: marcado pela brevidade da expressão ou afirmação: livre de toda elaboração e detalhes supérfluos. Apenas jogando isso lá fora ...
Dialock 14/02

Respostas:


25

A maneira mais simples é provavelmente obter o ângulo do vetor usando atan2(), como sugere o Tetrad nos comentários, e depois escalá-lo e arredondá-lo, por exemplo (pseudocódigo):

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

A octant = round( 8 * angle / (2*PI) + 8 ) % 8linha pode precisar de alguma explicação. Em praticamente todas as línguas que conheço, a atan2()função retorna o ângulo em radianos. Dividir por 2 π converte-o de radianos em frações de um círculo completo e, multiplicando por 8, converte-o em oitavos de um círculo, que arredondamos para o número inteiro mais próximo. Por fim, reduzimos o módulo 8 para cuidar do contorno, de modo que 0 e 8 sejam mapeados corretamente para o leste.

A razão para o + 8que ignorei acima é que em alguns idiomas atan2()pode retornar resultados negativos (por exemplo, de - π a + π em vez de 0 a 2 π ) e o operador módulo ( %) pode ser definido para retornar valores negativos para argumentos negativos (ou seu comportamento para argumentos negativos pode ser indefinido). Adicionando8 (ou seja, uma volta completa) à entrada antes da redução garante que os argumentos sejam sempre positivos, sem afetar o resultado de nenhuma outra maneira.

Se o seu idioma não fornecer uma função conveniente de arredondar para o mais próximo, você poderá usar uma conversão de truncamento de números inteiros e apenas adicionar 0,5 ao argumento, desta forma:

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

Observe que, em alguns idiomas, a conversão padrão de float para número inteiro arredonda as entradas negativas para cima em direção a zero, e não para baixo, o que é outro motivo para garantir que a entrada seja sempre positiva.

Obviamente, você pode substituir todas as ocorrências 8dessa linha por algum outro número (por exemplo, 4 ou 16, ou mesmo 6 ou 12, se você estiver em um mapa hexadecimal) para dividir o círculo em várias direções. Apenas ajuste a enumeração / matriz de acordo.


Note que geralmente atan2(y,x)não atan2(x,y).
23613 Sam sami hocevar

@ Sam: Opa, corrigido. Obviamente, atan2(x,y)também funcionaria se alguém listasse os cabeçalhos da bússola no sentido horário a partir do norte.
Ilmari Karonen

2
+1 por sinal, eu realmente acho que essa é a resposta mais direta e rigorosa.
sam Hocevar

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

1
Note que isso pode ser facilmente convertido para um 4-way bússola: quadtant = round(4 * angle / (2*PI) + 4) % 4e usando enum: { E, N, W, S }.
Spoike 13/05

10

Você tem 8 opções (ou 16 ou mais, se desejar uma precisão ainda mais fina).

insira a descrição da imagem aqui

Use atan2(y,x)para obter o ângulo do seu vetor.

atan2() funciona da seguinte maneira:

insira a descrição da imagem aqui

Então x = 1, y = 0 resultará em 0 e é descontínuo em x = -1, y = 0, contendo π e -π.

Agora, apenas precisamos mapear a saída atan2()para corresponder à da bússola que temos acima.

Provavelmente o mais simples de implementar é uma verificação incremental dos ângulos. Aqui estão alguns pseudo códigos que podem ser modificados facilmente para aumentar a precisão:

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

Agora, para adicionar mais precisão, basta adicionar os valores à enumeração da direção.

O algoritmo funciona verificando valores crescentes ao redor da bússola para ver se nosso ângulo está em algum lugar entre a última verificação e a nova posição. É por isso que começamos com -PI + increment / 2. Queremos compensar nossas verificações para incluir espaço igual em cada direção. Algo assim:

insira a descrição da imagem aqui

West é dividido em dois por causa dos valores de retorno de atan2()West serem descontínuos.


4
Uma maneira fácil de "convertê-los em um ângulo" é usar atan2, embora tenha em mente que 0 graus provavelmente seria leste e não norte.
Tétrada

1
Você não precisa das angle >=verificações no código acima; por exemplo, se o ângulo for menor que 45, o norte já terá sido retornado, assim você não precisa verificar se o ângulo> = 45 para a verificação leste. Da mesma forma, você não precisa de nenhum cheque antes de retornar ao oeste - é a única possibilidade restante.
MrKWatkins

4
Eu não chamaria isso de uma maneira concisa de obter a direção. Parece bastante desajeitado e exigirá muitas alterações para adaptar isso a diferentes "resoluções". Para não falar de uma tonelada de ifdeclarações, se você quiser seguir 16 direções ou mais.
bummzack

2
Não é necessário normalizar o vetor: o ângulo permanece o mesmo com as mudanças de magnitude.
Kylotan

Obrigado @bummzack, editei a postagem para torná-la mais concisa e fácil aumentar a precisão apenas adicionando mais valores de enumeração.
MichaelHouse

8

Sempre que estiver lidando com vetores, considere operações fundamentais de vetores em vez de converter em ângulos em algum quadro específico.

Dado um vetor de consulta ve um conjunto de vetores de unidade s, o vetor mais alinhado é o vetor s_ique maximiza dot(v,s_i). Isso se deve ao fato de que o produto escalar dado comprimentos fixos para os parâmetros tem um máximo para vetores com a mesma direção e um mínimo para vetores com direções opostas, mudando suavemente entre eles.

Isso generaliza trivialmente em mais dimensões que duas, é extensível com direções arbitrárias e não sofre problemas específicos de quadros, como gradientes infinitos.

Em termos de implementação, isso se resumiria à associação de um vetor em cada direção cardinal a um identificador (enum, string, o que você precisar) representando essa direção. Em seguida, você percorreria seu conjunto de instruções, encontrando a que apresentasse o produto de ponto mais alto.

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
Essa implementação também pode ser gravada sem ramificação e vetorizada sem muita dificuldade.
Promit

1
A mapcom float2como a chave? Isso não parece muito sério.
21813 Sam Hocevar

É "pseudo-código" de maneira didática. Se você deseja implementações otimizadas para pânico, o GDSE provavelmente não é o lugar para você usar sua cópia em massa. Quanto ao uso de float2 como chave, um float pode representar exatamente todos os números que usamos aqui, e você pode fazer um comparador perfeitamente bom para eles. As chaves de ponto flutuante são inadequadas apenas se contiverem valores especiais ou se você tentar procurar resultados computados. A iteração sobre uma sequência associativa é boa. Eu poderia ter usado uma pesquisa linear em uma matriz, com certeza, mas seria apenas uma confusão inútil.
Lars Viklund

3

Uma maneira que não foi mencionada aqui é tratar os vetores como números complexos. Eles não exigem trigonometria e podem ser bastante intuitivos para adicionar, multiplicar ou arredondar rotações, especialmente porque você já tem seus cabeçalhos representados como pares de números.

Caso você não esteja familiarizado com elas, as direções são expressas na forma de a + b (i), sendo um componente real eb (i) é o imaginário. Se você imaginar o plano cartesiano com o X sendo real e Y sendo imaginário, 1 seria leste (à direita), eu seria norte.

Aqui está a parte principal: As 8 direções cardinais são representadas exclusivamente com os números 1, -1 ou 0 para seus componentes reais e imaginários. Então, tudo o que você precisa fazer é reduzir as coordenadas X, Y como uma proporção e arredondar as duas para o número inteiro mais próximo para obter a direção.

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

Para a conversão de diagonal para a posição mais próxima, reduza X e Y proporcionalmente para que o valor maior seja exatamente 1 ou -1. Conjunto

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

Arredondar os dois componentes do que era originalmente (10, -2) fornece 1 + 0 (i) ou 1. Portanto, a direção mais próxima é leste.

O que foi dito acima não exige o uso de uma estrutura numérica complexa, mas pensar nelas como tal torna mais rápido encontrar as oito direções principais. Você pode fazer a matemática vetorial da maneira usual se quiser obter o cabeçalho líquido de dois ou mais vetores. (Como números complexos, você não adiciona, mas multiplica para o resultado)


1
Isso é incrível, mas comete um erro semelhante ao que cometi em minha própria tentativa. As respostas são próximas, mas não corretas. O ângulo limite entre E e NE é 22,5 graus, mas isso é cortado em 26,6 graus.
izb

Max(x, y)deve ser Max(Abs(x, y))trabalhar para os quadrantes negativos. Eu tentei e obtive o mesmo resultado que o izb - isso muda as direções da bússola em ângulos errados. Eu acho que ele mudaria quando o cabeçalho.y / o cabeçalho.x cruza 0,5 (então o valor arredondado muda de 0 para 1), que é arctan (0,5) = 26,565 °.
Amitp

Uma maneira diferente de usar números complexos aqui é observar que a multiplicação de números complexos envolve uma rotação. Se você construir um número complexo que represente 1/8 de uma rotação em torno de um círculo, toda vez que multiplicar por ele, moverá um octante. Então você poderia perguntar: podemos contar quantas multiplicações foram necessárias para ir do leste até o cabeçalho atual? A resposta para "quantas vezes precisamos multiplicar por isso" é um logaritmo . Se você procurar logaritmos para números complexos ... ele usa atan2. Então isso acaba sendo equivalente à resposta de Ilmari.
Amitp

-2

isso parece funcionar:

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

por que isso está sendo rejeitado?
Ray Tayek

Provavelmente porque não há explicação por trás do seu código. Por que essa é a solução e como funciona?
Vaillancourt

você correu?
Ray Tayek 25/01

Não, e considerando o nome da turma, presumi que sim e funcionou. E isso é ótimo. Mas você perguntou por que as pessoas votaram negativamente e eu respondi; Eu nunca impliquei que não funcionava :)
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 + sinal (x)) * (1 sinal (y ^ 2)) - (2 + sinal (x)) * sinal (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)

Por enquanto, esse é apenas um monte de personagens que não fazem muito sentido; por que essa é uma solução que funcionaria para a pergunta, como funciona?
Vaillancourt

Eu escrevo a fórmula como escrevi jn excel e está funcionando perfeitamente.
theodore panagos 24/01

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

-4

Quando você quer uma string:

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)

Isso fornece constantes utilizando campos de bits:

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

Uma ligeira melhoria de desempenho seria colocar os <-checks no ramo else dos >-checks correspondentes , mas me abstive de fazer isso porque isso prejudica a legibilidade.


2
Desculpe, mas isso não dará exatamente a resposta que estou procurando. Com esse código, ele só produzirá "N" se o vetor estiver precisamente norte, e NE ou NW se x for qualquer outro valor. O que precisa é a direcção da bússola mais próximo, por exemplo, se o vector é para mais perto do que N NW, então ele irá produzir N.
izb

Isso realmente daria a direção mais próxima? Parece que um vetor de (0,00001,100) lhe daria o nordeste. edit: você me venceu izb.
CiscoIPPhone

você não disse que quer a direção mais próxima.
Philipp

1
Desculpe, eu escondi isso no título. Deveria ter sido mais claro no corpo da pergunta #
23413 izb

1
Que tal usar a norma infinita? Dividir por max (abs (vector.components)) fornece um vetor normalizado com relação a essa norma. Agora você pode escrever uma pequena tabela de check-up com base em if (x > 0.9) dir |= DIR_Etodo o resto. Deve ser melhor que o código original de Phillipp e um pouco mais barato do que usar a norma L2 e atan2. Talvez sim, talvez não.
Teodron
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.