Quelle est la meilleure façon de transformer un vecteur 2D dans la direction de la boussole à 8 directions la plus proche?

17

Si vous avez un vecteur 2D exprimé en x et y, quelle est la bonne façon de le transformer dans la direction de la boussole la plus proche?

par exemple

x:+1,  y:+1 => NE
x:0,   y:+3 => N
x:+10, y:-2 => E   // closest compass direction
izb
la source
le voulez-vous comme une chaîne ou une énumération? (oui, ça compte)
Philipp
Soit, car il sera utilisé dans les deux sens :) Bien que si je devais choisir, je prendrais une chaîne.
izb
1
Êtes-vous également préoccupé par la performance, ou seulement par la concision?
Marcin Seredynski
2
angle var = Math.atan2 (y, x); return <Direction> Math.floor ((Math.round (angle / (2 * Math.PI / 8)) + 8 + 2)% 8); J'utilise celui-ci
Kikaimaru
Concis: marqué par la brièveté de l'expression ou de la déclaration: libre de toute élaboration et de tout détail superflu. Il suffit de jeter ça là-bas ...
Dialock

Réponses:

25

Le moyen le plus simple est probablement d'obtenir l'angle du vecteur en utilisant atan2(), comme Tetrad le suggère dans les commentaires, puis de le mettre à l'échelle et de l'arrondir, par exemple (pseudocode):

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

La octant = round( 8 * angle / (2*PI) + 8 ) % 8ligne pourrait avoir besoin d'explications. Dans à peu près toutes les langues que je connais, la atan2()fonction renvoie l'angle en radians. Le diviser par 2 π le convertit des radians en fractions d'un cercle complet, et le multiplier par 8 le convertit ensuite en huitièmes de cercle, que nous arrondissons ensuite à l'entier le plus proche. Enfin, nous le réduisons modulo 8 pour prendre en charge le bouclage, afin que les deux 0 et 8 soient correctement mappés vers l'est.

La raison de la + 8, que j'ai ignorée ci-dessus, est que dans certaines langues atan2()peut retourner des résultats négatifs (c'est-à-dire de - π à + π plutôt que de 0 à 2 π ) et l'opérateur modulo ( %) peut être défini pour renvoyer des valeurs négatives pour arguments négatifs (ou son comportement pour les arguments négatifs peut être indéfini). L'ajout 8(c'est-à-dire un tour complet) à l'entrée avant la réduction garantit que les arguments sont toujours positifs, sans affecter le résultat d'une autre manière.

Si votre langue ne fournit pas une fonction arrondie au plus proche, vous pouvez utiliser une conversion d'entier tronqué à la place et ajouter simplement 0,5 à l'argument, comme ceci:

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

Notez que, dans certaines langues, la conversion flottante en entier par défaut arrondit les entrées négatives vers le haut plutôt que vers le bas, ce qui est une autre raison pour s'assurer que l'entrée est toujours positive.

Bien sûr, vous pouvez remplacer toutes les occurrences de 8sur cette ligne par un autre nombre (par exemple 4 ou 16, ou même 6 ou 12 si vous êtes sur une carte hexadécimale) pour diviser le cercle dans autant de directions. Ajustez simplement l'énumération / le tableau en conséquence.

Ilmari Karonen
la source
Notez que ce n'est généralement atan2(y,x)pas le cas atan2(x,y).
sam hocevar
@Sam: Oups, corrigé. Bien sûr, atan2(x,y)cela fonctionnerait aussi, si l'on listait simplement les en-têtes de la boussole dans le sens des aiguilles d'une montre à partir du nord à la place.
Ilmari Karonen
2
+1 au fait, je pense vraiment que c'est la réponse la plus simple et la plus rigoureuse.
sam hocevar
1
@TheLima:octant = round(8 * angle / 360 + 8) % 8
Ilmari Karonen
1
Notez que cette façon peut facilement être converti en une boussole à 4 voies: quadtant = round(4 * angle / (2*PI) + 4) % 4et l' utilisation ENUM: { E, N, W, S }.
Spoike
10

Vous avez 8 options (ou 16 ou plus si vous voulez une précision encore plus fine).

entrez la description de l'image ici

Utilisez atan2(y,x)pour obtenir l'angle de votre vecteur.

atan2() fonctionne de la manière suivante:

entrez la description de l'image ici

Donc x = 1, y = 0 donnera 0, et il est discontinu à x = -1, y = 0, contenant à la fois π et -π.

Maintenant, nous avons juste besoin de mapper la sortie de atan2()pour correspondre à celle de la boussole que nous avons ci-dessus.

Le plus simple à mettre en œuvre est probablement une vérification incrémentielle des angles. Voici un pseudo-code qui peut être facilement modifié pour une précision accrue:

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

Maintenant, pour ajouter plus de précision, ajoutez simplement les valeurs à l'énumération de la direction.

L'algorithme fonctionne en vérifiant les valeurs croissantes autour de la boussole pour voir si notre angle se situe quelque part entre l'endroit où nous avons vérifié pour la dernière fois et la nouvelle position. C'est pourquoi nous commençons à -PI + incrément / 2. Nous voulons compenser nos contrôles pour inclure un espace égal autour de chaque direction. Quelque chose comme ça:

entrez la description de l'image ici

West est divisé en deux car les valeurs de retour de atan2()at West sont discontinues.

MichaelHouse
la source
4
Un moyen simple de les "convertir en un angle" consiste à les utiliser atan2, mais gardez à l'esprit que 0 degré serait probablement à l'est et non au nord.
Tetrad
1
Vous n'avez pas besoin des angle >=vérifications dans le code ci-dessus; par exemple, si l'angle est inférieur à 45, le nord aura déjà été renvoyé, vous n'avez donc pas besoin de vérifier si l'angle> = 45 pour le contrôle est. De même, vous n'avez besoin d'aucun chèque avant de retourner vers l'ouest - c'est la seule possibilité qui reste.
MrKWatkins
4
Je n'appellerais pas cela un moyen concis pour obtenir la direction. Cela semble plutôt maladroit et nécessitera beaucoup de changements pour l'adapter aux différentes "résolutions". Sans parler d'une tonne de ifdéclarations si vous voulez aller dans 16 directions ou plus.
bummzack
2
Pas besoin de normaliser le vecteur: l'angle reste le même lors des changements de magnitude.
Kylotan
Merci @bummzack, j'ai édité le message pour le rendre plus concis et facile à augmenter la précision simplement en ajoutant plus de valeurs d'énumération.
MichaelHouse
8

Chaque fois que vous traitez avec des vecteurs, envisagez des opérations vectorielles fondamentales au lieu de convertir en angles dans un cadre particulier.

Étant donné un vecteur de requête vet un ensemble de vecteurs unitaires s, le vecteur le plus aligné est le vecteur s_iqui maximisedot(v,s_i) . Cela est dû au fait que le produit scalaire donné des longueurs fixes pour les paramètres a un maximum pour les vecteurs avec la même direction et un minimum pour les vecteurs avec des directions opposées, changeant en douceur entre les deux.

Cela se généralise trivialement en plus de deux dimensions, est extensible avec des directions arbitraires et ne souffre pas de problèmes spécifiques à l'image comme des gradients infinis.

En termes d'implémentation, cela reviendrait à associer à partir d'un vecteur dans chaque direction cardinale un identifiant (énum, ​​chaîne, tout ce dont vous avez besoin) représentant cette direction. Vous feriez ensuite une boucle sur votre ensemble de directions, en trouvant celle avec le produit scalaire le plus élevé.

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
la source
2
Cette implémentation peut également être écrite sans branche et vectorisée sans trop de problèmes.
Promit le
1
Un mapavec float2comme clé? Cela n'a pas l'air très sérieux.
sam hocevar
C'est du "pseudo-code" de manière didactique. Si vous voulez des implémentations optimisées pour la panique, GDSE n'est probablement pas l'endroit où aller pour vos pâtes à copier. En ce qui concerne l'utilisation de float2 comme clé, un float peut représenter exactement les nombres entiers que nous utilisons ici, et vous pouvez en faire un parfait comparateur. Les clés à virgule flottante ne conviennent que si elles contiennent des valeurs spéciales ou si vous essayez de rechercher des résultats calculés. Itérer sur une séquence associative est très bien. J'aurais pu utiliser une recherche linéaire dans un tableau, bien sûr, mais ce serait juste un encombrement inutile.
Lars Viklund
3

Une façon qui n'a pas été mentionnée ici est de traiter les vecteurs comme des nombres complexes. Ils ne nécessitent pas de trigonométrie et peuvent être assez intuitifs pour ajouter, multiplier ou arrondir les rotations, d'autant plus que vos en-têtes sont déjà représentés sous forme de paires de nombres.

Dans le cas où vous ne les connaissez pas, les directions sont exprimées sous la forme a + b (i) avec a étant la composante réelle et b (i) est l'imaginaire. Si vous imaginez le plan cartésien avec X réel et Y imaginaire, 1 serait à l'est (à droite), je serais au nord.

Voici la partie clé: Les 8 directions cardinales sont représentées exclusivement avec les nombres 1, -1 ou 0 pour leurs composantes réelles et imaginaires. Donc, tout ce que vous avez à faire est de réduire vos coordonnées X, Y comme un rapport et d'arrondir les deux au nombre entier le plus proche pour obtenir la direction.

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

Pour la conversion de la diagonale en tête vers la plus proche, réduisez X et Y proportionnellement pour que la plus grande valeur soit exactement 1 ou -1. Ensemble

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

Arrondir les deux composantes de ce qui était à l'origine (10, -2) vous donne 1 + 0 (i) ou 1. La direction la plus proche est donc l'est.

Ce qui précède ne nécessite pas réellement l'utilisation d'une structure numérique complexe, mais en les considérant comme tels, il est plus rapide de trouver les 8 directions cardinales. Vous pouvez effectuer des calculs vectoriels de la manière habituelle si vous souhaitez obtenir l'en-tête net de deux ou plusieurs vecteurs. (En tant que nombres complexes, vous n'ajoutez pas, mais multipliez pour le résultat)

ChrisC
la source
1
C'est génial, mais fait une erreur similaire à celle que j'ai faite lors de ma propre tentative. Les réponses sont proches mais pas correctes. L'angle limite entre E et NE est de 22,5 degrés, mais il se coupe à 26,6 degrés.
izb
Max(x, y)devrait être Max(Abs(x, y))de travailler pour les quadrants négatifs. Je l'ai essayé et j'ai obtenu le même résultat que izb - cela change les directions de la boussole aux mauvais angles. Je suppose que cela changerait lorsque le cap.y / cap.x croise 0,5 (donc la valeur arrondie passe de 0 à 1), ce qui est arctan (0,5) = 26,565 °.
amitp
Une manière différente d'utiliser des nombres complexes ici est d'observer que la multiplication des nombres complexes implique une rotation. Si vous construisez un nombre complexe qui représente 1/8 d'une rotation autour d'un cercle, alors à chaque fois que vous le multipliez, vous déplacez un octant. Vous pourriez donc vous demander: peut-on compter combien de multiplications a-t-il fallu pour passer de l'Est à la rubrique actuelle? La réponse à "combien de fois devons-nous multiplier par ceci" est un logarithme . Si vous recherchez des logarithmes pour des nombres complexes… il utilise atan2. Donc cela finit par être équivalent à la réponse d'Ilmari.
amitp
-2

cela semble fonctionner:

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
la source
pourquoi est-ce rejeté?
Ray Tayek
Très probablement parce qu'il n'y a aucune explication derrière votre code. Pourquoi est-ce la solution et comment ça marche?
Vaillancourt
l'avez-vous exécuté?
Ray Tayek
Non, et étant donné le nom de la classe, je suppose que vous l'avez fait et cela a fonctionné. Et c'est super. Mais vous avez demandé pourquoi les gens ont voté contre et j'ai répondu; Je n'ai jamais laissé entendre que cela n'a pas fonctionné :)
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 + signe (x)) * (1-signe (y ^ 2)) - (2 + signe (x)) * signe (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
la source
Pour l'instant, ce n'est qu'un groupe de personnages qui n'ont pas beaucoup de sens; pourquoi est-ce une solution qui fonctionnerait pour la question, comment ça marche?
Vaillancourt
J'écris la formule comme j'ai écrit jn excel et fonctionne parfaitement.
theodore panagos
= 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 * SIGNE ((X1 ^ 2-Y1 ^ 2) * (X1 * Y1))), 8)
theodore panagos
-4

Lorsque vous voulez une chaîne:

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)

Cela vous donne des constantes en utilisant des champs 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

Une légère amélioration des performances serait de < -checks dans la branche else des >-checks correspondants , mais je me suis abstenu de le faire car cela nuit à la lisibilité.

Philipp
la source
2
Désolé, mais cela ne donnera pas exactement la réponse que je cherche. Avec ce code, il ne donnera "N" que si le vecteur est précisément au nord, et NE ou NW si x est une autre valeur. Ce dont j'ai besoin, c'est de la direction de la boussole la plus proche, par exemple si le vecteur est plus proche de N que NW, il produira N.
izb
Cela donnerait-il la direction la plus proche? Il semble qu'un vecteur de (0,00001,100) vous donnerait le nord-est. edit: tu m'as battu dessus izb.
CiscoIPPhone
vous n'avez pas dit que vous vouliez la direction la plus proche.
Philipp
1
Désolé, j'ai caché ça dans le titre. Cela aurait dû être plus clair dans le corps de la question
izb
1
Qu'en est-il de l'utilisation de la norme infinie? La division par max (abs (vector.components)) vous donne un vecteur normalisé par rapport à cette norme. Maintenant, vous pouvez écrire un petit tableau de contrôle basé sur if (x > 0.9) dir |= DIR_Eet tout le reste. Il devrait être meilleur que le code original de Phillipp et un peu moins cher que d'utiliser la norme L2 et atan2. Peut etre ou peut etre pas.
teodron