Trier les points dans le sens des aiguilles d'une montre?

156

Étant donné un tableau de points x, y, comment trier les points de ce tableau dans le sens des aiguilles d'une montre (autour de leur point central moyen global)? Mon objectif est de passer les points à une fonction de création de ligne pour aboutir à quelque chose qui semble plutôt "solide", aussi convexe que possible sans intersection de lignes.

Pour ce que ça vaut, j'utilise Lua, mais n'importe quel pseudocode serait apprécié.

Mise à jour: Pour référence, voici le code Lua basé sur l'excellente réponse de Ciamej (ignorez mon préfixe "app"):

function appSortPointsClockwise(points)
    local centerPoint = appGetCenterPointOfPoints(points)
    app.pointsCenterPoint = centerPoint
    table.sort(points, appGetIsLess)
    return points
end

function appGetIsLess(a, b)
    local center = app.pointsCenterPoint

    if a.x >= 0 and b.x < 0 then return true
    elseif a.x == 0 and b.x == 0 then return a.y > b.y
    end

    local det = (a.x - center.x) * (b.y - center.y) - (b.x - center.x) * (a.y - center.y)
    if det < 0 then return true
    elseif det > 0 then return false
    end

    local d1 = (a.x - center.x) * (a.x - center.x) + (a.y - center.y) * (a.y - center.y)
    local d2 = (b.x - center.x) * (b.x - center.x) + (b.y - center.y) * (b.y - center.y)
    return d1 > d2
end

function appGetCenterPointOfPoints(points)
    local pointsSum = {x = 0, y = 0}
    for i = 1, #points do pointsSum.x = pointsSum.x + points[i].x; pointsSum.y = pointsSum.y + points[i].y end
    return {x = pointsSum.x / #points, y = pointsSum.y / #points}
end

Philipp Lenssen
la source
1
Pensez à calculer l'angle de la ligne radiale passant par ce point. Ensuite, triez par angle.
Président James K. Polk
Au cas où vous ne le sauriez pas, lua a une fonction intégrée ipairs(tbl)qui itère sur les indices et les valeurs de tbl de 1 à #tbl. Donc, pour le calcul de la somme, vous pouvez le faire, ce que la plupart des gens trouvent plus propre:for _, p in ipairs(points) do pointsSum.x = pointsSum.x + p.x; pointsSum.y = pointsSum.y + p.y end
Ponkadoodle
2
@Wallacoloo C'est très discutable. De plus, dans la vanille, Lua ipairsest nettement plus lent que la boucle for numérique.
Alexander Gladysh
J'ai dû faire quelques petits changements pour que cela fonctionne pour mon cas (il suffit de comparer deux points par rapport à un centre). gist.github.com/personalnadir/6624172 Toutes ces comparaisons à 0 dans le code semblent supposer que les points sont répartis autour de l'origine, par opposition à un point arbitraire. Je pense également que la première condition triera les points sous le point central de manière incorrecte. Merci pour le code, cela a été vraiment utile!
personalnadir

Réponses:

192

Commencez par calculer le point central. Triez ensuite les points en utilisant l'algorithme de tri de votre choix, mais utilisez une routine de comparaison spéciale pour déterminer si un point est inférieur à l'autre.

Vous pouvez vérifier si un point (a) est à gauche ou à droite de l'autre (b) par rapport au centre par ce simple calcul:

det = (a.x - center.x) * (b.y - center.y) - (b.x - center.x) * (a.y - center.y)

si le résultat est zéro, alors ils sont sur la même ligne à partir du centre, si c'est positif ou négatif, alors c'est d'un côté ou de l'autre, donc un point précédera l'autre. En l'utilisant, vous pouvez construire une relation inférieure à pour comparer les points et déterminer l'ordre dans lequel ils doivent apparaître dans le tableau trié. Mais vous devez définir où est le début de cet ordre, je veux dire quel angle sera l'angle de départ (par exemple la moitié positive de l'axe des x).

Le code de la fonction de comparaison peut ressembler à ceci:

bool less(point a, point b)
{
    if (a.x - center.x >= 0 && b.x - center.x < 0)
        return true;
    if (a.x - center.x < 0 && b.x - center.x >= 0)
        return false;
    if (a.x - center.x == 0 && b.x - center.x == 0) {
        if (a.y - center.y >= 0 || b.y - center.y >= 0)
            return a.y > b.y;
        return b.y > a.y;
    }

    // compute the cross product of vectors (center -> a) x (center -> b)
    int det = (a.x - center.x) * (b.y - center.y) - (b.x - center.x) * (a.y - center.y);
    if (det < 0)
        return true;
    if (det > 0)
        return false;

    // points a and b are on the same line from the center
    // check which point is closer to the center
    int d1 = (a.x - center.x) * (a.x - center.x) + (a.y - center.y) * (a.y - center.y);
    int d2 = (b.x - center.x) * (b.x - center.x) + (b.y - center.y) * (b.y - center.y);
    return d1 > d2;
}

Cela ordonnera les points dans le sens des aiguilles d'une montre à partir de 12 heures. Les points sur la même «heure» seront commandés à partir de ceux qui sont plus éloignés du centre.

Si vous utilisez des types entiers (qui ne sont pas vraiment présents dans Lua), vous devez vous assurer que les variables det, d1 et d2 sont d'un type qui pourra contenir le résultat des calculs effectués.

Si vous voulez obtenir quelque chose qui a l'air solide, aussi convexe que possible, alors je suppose que vous recherchez une coque convexe . Vous pouvez le calculer à l'aide du Graham Scan . Dans cet algorithme, vous devez également trier les points dans le sens horaire (ou anti-horaire) à partir d'un point de pivot spécial. Ensuite, vous répétez des étapes de boucle simples à chaque fois en vérifiant si vous tournez à gauche ou à droite en ajoutant de nouveaux points à la coque convexe, cette vérification est basée sur un produit croisé, tout comme dans la fonction de comparaison ci-dessus.

Éditer:

Ajout d'une instruction if supplémentaire if (a.y - center.y >= 0 || b.y - center.y >=0)pour s'assurer que les points qui ont x = 0 et y négatif sont triés en commençant par ceux qui sont plus éloignés du centre. Si vous ne vous souciez pas de l'ordre des points sur la même «heure», vous pouvez omettre cette instruction if et toujours revenir a.y > b.y.

Correction des premières instructions if avec l'ajout -center.xet-center.y .

Ajout de la deuxième instruction if (a.x - center.x < 0 && b.x - center.x >= 0). C'était un oubli évident qu'il manquait. Les instructions if pourraient être réorganisées maintenant car certains contrôles sont redondants. Par exemple, si la première condition de la première instruction if est fausse, la première condition de la seconde if doit être vraie. J'ai cependant décidé de laisser le code tel quel par souci de simplicité. Il est fort possible que le compilateur optimise le code et produise de toute façon le même résultat.

ciamej
la source
25
+1: Non atan(), pas de racine carrée et même pas de division. Ceci est un bon exemple de pensée infographique. Éliminez tous les cas faciles dès que possible, et même dans les cas difficiles, calculez le moins possible pour connaître la réponse requise.
RBerteig
Mais cela nécessite de comparer tous les points à tous les autres. Existe-t-il une méthode simple pour insérer de nouveaux points?
Iterator
2
si l'ensemble de points est connu a priori, il ne prend que O (n * log n) comparaisons. Si vous souhaitez ajouter des points entre-temps, vous devez les conserver dans un ensemble trié tel qu'un arbre de recherche binaire équilibré. Dans un tel cas, l'ajout d'un nouveau point nécessite des comparaisons O (log n) et c'est exactement la même chose pour la solution impliquant des coordonnées polaires.
ciamej
2
Est-ce qu'il manque le cas: if (ax - center.x <0 && bx - center.x> = 0) return false;
Tom Martin
2
Héy. C'est assez vieux, mais: "Cela ordonnera les points dans le sens des aiguilles d'une montre à partir de 12 heures." Pourquoi 12 heures et comment puis-je le changer à 6 heures? Quelqu'un peut-il me le dire?
Ismoh
20

Une approche alternative intéressante à votre problème serait de trouver le minimum approximatif au problème du voyageur de commerce (TSP), c'est-à-dire. l'itinéraire le plus court reliant tous vos points. Si vos points forment une forme convexe, cela devrait être la bonne solution, sinon, cela devrait toujours avoir l'air bien (une forme "solide" peut être définie comme une forme qui a un faible rapport périmètre / surface, ce que nous optimisons ici) .

Vous pouvez utiliser n'importe quelle implémentation d'un optimiseur pour le TSP, dont je suis sûr que vous pouvez en trouver une tonne dans la langue de votre choix.

static_rtti
la source
Yikes. «Intéressant» est un euphémisme. :)
Iterator
@Iterator: J'étais assez content de mon idée, j'ai été assez déçu de me voir décliner: - / Pensez-vous que c'est valable?
static_rtti
1
Je suggérais d'utiliser l'une des nombreuses approximations rapides, et non l'algorithme original NP-complet, bien sûr.
static_rtti
6
J'apprécie l'angle supplémentaire! Avoir plusieurs réponses valides, bien que très différentes, pourrait être d'une grande aide si quelqu'un à l'avenir tombe sur ce fil à la recherche d'options de brainstorming.
Philipp Lenssen
1
Notez que mon approche est probablement plus lente, mais plus correcte dans les cas complexes: imaginez le cas où les points pour un "8", par exemple. Les coordonnées polaires ne vous aideront pas dans ce cas, et le résultat que vous obtiendrez dépendra fortement du centre que vous aurez choisi. La solution TSP est indépendante de tout paramètre «heuristique».
static_rtti
19

Ce que vous demandez, c'est un système appelé coordonnées polaires . La conversion des coordonnées cartésiennes en coordonnées polaires se fait facilement dans n'importe quelle langue. Les formules se trouvent dans cette section .

Après la conversion en coordonnées polaires, triez simplement par l'angle, thêta.

Itérateur
la source
4
Cela fonctionnera, mais cela aura aussi le défaut de faire plus de calculs que nécessaire pour répondre à la question de commande. En pratique, vous ne vous souciez ni des angles réels ni des distances radiales, mais simplement de leur ordre relatif. La solution de ciamej est meilleure car elle évite les divisions, les racines carrées et le trig.
RBerteig
1
Je ne sais pas quel est votre critère pour «mieux». Par exemple, comparer tous les points les uns aux autres est une sorte de perte de calcul. Trig n'est pas quelque chose qui effraie les adultes, n'est-ce pas?
Iterator
3
Ce n'est pas que le déclencheur soit effrayant. Le problème est que le déclenchement est coûteux à calculer et n'était pas nécessaire pour déterminer l'ordre relatif des angles. De même, vous n'avez pas besoin de prendre les racines carrées pour mettre les rayons en ordre. Une conversion complète des coordonnées cartésiennes en coordonnées polaires fera à la fois un arc-tangent et une racine carrée. Par conséquent, votre réponse est correcte, mais dans le contexte de l'infographie ou de la géométrie informatique, ce n'est probablement pas la meilleure façon de le faire.
RBerteig
Je l'ai. Cependant, l'OP n'a pas posté en tant que comp-geo, c'était une balise par quelqu'un d'autre. Pourtant, il semble que l'autre solution soit polynomiale dans le nombre de points, ou est-ce que je me trompe? Si c'est le cas, cela brûle plus de cycles que trig.
Iterator
Je n'avais pas vraiment remarqué la balise comp-geo, j'ai juste supposé que les seules applications rationnelles pour la question devaient être l'une ou l'autre. Après tout, la question de performance devient sans objet s'il n'y a que quelques points, et / ou l'opération sera effectuée assez rarement. À ce stade, savoir comment le faire devient important et c'est pourquoi je conviens que votre réponse est correcte. Il explique comment calculer la notion d '«ordre dans le sens des aiguilles d'une montre» en des termes qui peuvent être expliqués à n'importe qui.
RBerteig
3

Une autre version (retourne true si a vient avant b dans le sens antihoraire):

    bool lessCcw(const Vector2D &center, const Vector2D &a, const Vector2D &b) const
    {
        // Computes the quadrant for a and b (0-3):
        //     ^
        //   1 | 0
        //  ---+-->
        //   2 | 3

        const int dax = ((a.x() - center.x()) > 0) ? 1 : 0;
        const int day = ((a.y() - center.y()) > 0) ? 1 : 0;
        const int qa = (1 - dax) + (1 - day) + ((dax & (1 - day)) << 1);

        /* The previous computes the following:

           const int qa =
           (  (a.x() > center.x())
            ? ((a.y() > center.y())
                ? 0 : 3)
            : ((a.y() > center.y())
                ? 1 : 2)); */

        const int dbx = ((b.x() - center.x()) > 0) ? 1 : 0;
        const int dby = ((b.y() - center.y()) > 0) ? 1 : 0;
        const int qb = (1 - dbx) + (1 - dby) + ((dbx & (1 - dby)) << 1);

        if (qa == qb) {
            return (b.x() - center.x()) * (a.y() - center.y()) < (b.y() - center.y()) * (a.x() - center.x());
        } else {
            return qa < qb;
       } 
    }

C'est plus rapide, car le compilateur (testé sur Visual C ++ 2015) ne génère pas de saut pour calculer dax, day, dbx, dby. Voici l'assembly de sortie du compilateur:

; 28   :    const int dax = ((a.x() - center.x()) > 0) ? 1 : 0;

    vmovss  xmm2, DWORD PTR [ecx]
    vmovss  xmm0, DWORD PTR [edx]

; 29   :    const int day = ((a.y() - center.y()) > 0) ? 1 : 0;

    vmovss  xmm1, DWORD PTR [ecx+4]
    vsubss  xmm4, xmm0, xmm2
    vmovss  xmm0, DWORD PTR [edx+4]
    push    ebx
    xor ebx, ebx
    vxorps  xmm3, xmm3, xmm3
    vcomiss xmm4, xmm3
    vsubss  xmm5, xmm0, xmm1
    seta    bl
    xor ecx, ecx
    vcomiss xmm5, xmm3
    push    esi
    seta    cl

; 30   :    const int qa = (1 - dax) + (1 - day) + ((dax & (1 - day)) << 1);

    mov esi, 2
    push    edi
    mov edi, esi

; 31   : 
; 32   :    /* The previous computes the following:
; 33   : 
; 34   :    const int qa =
; 35   :        (   (a.x() > center.x())
; 36   :         ? ((a.y() > center.y()) ? 0 : 3)
; 37   :         : ((a.y() > center.y()) ? 1 : 2));
; 38   :    */
; 39   : 
; 40   :    const int dbx = ((b.x() - center.x()) > 0) ? 1 : 0;

    xor edx, edx
    lea eax, DWORD PTR [ecx+ecx]
    sub edi, eax
    lea eax, DWORD PTR [ebx+ebx]
    and edi, eax
    mov eax, DWORD PTR _b$[esp+8]
    sub edi, ecx
    sub edi, ebx
    add edi, esi
    vmovss  xmm0, DWORD PTR [eax]
    vsubss  xmm2, xmm0, xmm2

; 41   :    const int dby = ((b.y() - center.y()) > 0) ? 1 : 0;

    vmovss  xmm0, DWORD PTR [eax+4]
    vcomiss xmm2, xmm3
    vsubss  xmm0, xmm0, xmm1
    seta    dl
    xor ecx, ecx
    vcomiss xmm0, xmm3
    seta    cl

; 42   :    const int qb = (1 - dbx) + (1 - dby) + ((dbx & (1 - dby)) << 1);

    lea eax, DWORD PTR [ecx+ecx]
    sub esi, eax
    lea eax, DWORD PTR [edx+edx]
    and esi, eax
    sub esi, ecx
    sub esi, edx
    add esi, 2

; 43   : 
; 44   :    if (qa == qb) {

    cmp edi, esi
    jne SHORT $LN37@lessCcw

; 45   :        return (b.x() - center.x()) * (a.y() - center.y()) < (b.y() - center.y()) * (a.x() - center.x());

    vmulss  xmm1, xmm2, xmm5
    vmulss  xmm0, xmm0, xmm4
    xor eax, eax
    pop edi
    vcomiss xmm0, xmm1
    pop esi
    seta    al
    pop ebx

; 46   :    } else {
; 47   :        return qa < qb;
; 48   :    }
; 49   : }

    ret 0
$LN37@lessCcw:
    pop edi
    pop esi
    setl    al
    pop ebx
    ret 0
?lessCcw@@YA_NABVVector2D@@00@Z ENDP            ; lessCcw

Prendre plaisir.

AGPX
la source
1
Les deux instructions de retour dans le commutateur sont mathématiquement équivalentes. Y a-t-il une raison pour avoir le changement?
unagi
0
  • vector3 a = nouveau vecteur3 (1, 0, 0) .............. wrt X_axis
  • vector3 b = any_point - Centre;
- y = |a * b|   ,   x =  a . b

- Atan2(y , x)...............................gives angle between -PI  to  + PI  in radians
- (Input % 360  +  360) % 360................to convert it from  0 to 2PI in radians
- sort by adding_points to list_of_polygon_verts by angle  we got 0  to 360

Enfin, vous obtenez des verts triés par Anticlockwize

list.Reverse () .................. Clockwise_order

Pavan
la source