Ordinare i punti in senso orario?


156

Dato un array di punti x, y, come posso ordinare i punti di questo array in senso orario (attorno al loro punto centrale medio complessivo)? Il mio obiettivo è passare i punti a una funzione di creazione di linee per finire con qualcosa che sembra piuttosto "solido", il più convesso possibile senza linee che si intersecano.

Per quello che vale, sto usando Lua, ma qualsiasi pseudocodice sarebbe apprezzato.

Aggiornamento: per riferimento, questo è il codice Lua basato sulla risposta eccellente di Ciamej (ignora il mio prefisso "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


1
Pensa a calcolare l'angolo della linea radiale attraverso quel punto. Quindi ordinare per angolo.
Presidente James K. Polk,

Nel caso in cui non lo sapessi, lua ha una funzione incorporata ipairs(tbl)che scorre sugli indici e sui valori di tbl da 1 a #tbl. Quindi, per il calcolo della somma, puoi farlo, che la maggior parte delle persone sembra più pulita:for _, p in ipairs(points) do pointsSum.x = pointsSum.x + p.x; pointsSum.y = pointsSum.y + p.y end
Ponkadoodle

2
@Wallacoloo Questo è altamente discutibile. Inoltre, in vaniglia Lua ipairsè significativamente più lenta di quella numerica per il ciclo.
Alexander Gladysh,

Ho dovuto apportare alcune piccole modifiche per farlo funzionare nel mio caso (solo confrontando due punti rispetto a un centro). gist.github.com/personalnadir/6624172 Tutti quei confronti con 0 nel codice sembrano supporre che i punti siano distribuiti attorno all'origine, al contrario di un punto arbitrario. Penso anche che la prima condizione ordinerà erroneamente i punti sotto il punto centrale. Grazie per il codice, è stato davvero utile!
personalnadir,

Risposte:


192

Innanzitutto, calcola il punto centrale. Quindi ordina i punti usando l'algoritmo di ordinamento che preferisci, ma usa una routine di confronto speciale per determinare se un punto è inferiore all'altro.

Con questo semplice calcolo puoi verificare se un punto (a) è a sinistra o a destra dell'altro (b) rispetto al centro:

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

se il risultato è zero, allora sono sulla stessa linea dal centro, se è positivo o negativo, quindi è da una parte o dall'altra, quindi un punto precederà l'altro. Usandolo puoi costruire una relazione minore di per confrontare i punti e determinare l'ordine in cui dovrebbero apparire nella matrice ordinata. Ma devi definire dov'è l'inizio di quell'ordine, intendo quale angolo sarà quello iniziale (es. La metà positiva dell'asse x).

Il codice per la funzione di confronto può essere simile al seguente:

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

Questo ordinerà i punti in senso orario a partire dalle ore 12. I punti sulla stessa "ora" verranno ordinati a partire da quelli che si trovano più lontano dal centro.

Se usi tipi interi (che non sono realmente presenti in Lua) dovresti assicurarti che le variabili det, d1 e d2 siano di un tipo che sarà in grado di contenere il risultato dei calcoli eseguiti.

Se vuoi ottenere qualcosa di solido, il più convesso possibile, suppongo che stai cercando uno scafo convesso . Puoi calcolarlo usando Graham Scan . In questo algoritmo, devi anche ordinare i punti in senso orario (o antiorario) a partire da un punto di articolazione speciale. Quindi ripeti semplici passaggi ad anello ogni volta che controlli se giri a sinistra o a destra aggiungendo nuovi punti allo scafo convesso, questo controllo si basa su un prodotto incrociato proprio come nella funzione di confronto sopra.

Modificare:

Aggiunta un'altra istruzione if if (a.y - center.y >= 0 || b.y - center.y >=0)per assicurarsi che i punti che hanno x = 0 e y negativi siano ordinati a partire da quelli che si trovano più lontano dal centro. Se non ti interessa l'ordine dei punti nella stessa "ora", puoi ometterlo in caso di dichiarazione e tornare sempre a.y > b.y.

Correzione delle prime istruzioni if ​​con l'aggiunta di -center.xe -center.y.

Aggiunta la seconda istruzione if (a.x - center.x < 0 && b.x - center.x >= 0). Era ovvio che mancava. Le istruzioni if ​​potrebbero essere riorganizzate ora perché alcuni controlli sono ridondanti. Ad esempio, se la prima condizione nella prima istruzione if è falsa, la prima condizione della seconda if deve essere vera. Ho deciso, tuttavia, di lasciare il codice per semplicità. È possibile che il compilatore ottimizzi il codice e produca lo stesso risultato comunque.


25
+1: No atan(), nessuna radice quadrata e persino nessuna divisione. Questo è un buon esempio di pensiero sulla computer grafica. Elimina tutti i casi facili il più presto possibile, e anche nei casi difficili, calcola il meno possibile per conoscere la risposta richiesta.
RBerteig,

Ma richiede di confrontare tutti i punti con tutti gli altri. Esiste un metodo semplice per inserire nuovi punti?
Iteratore

2
se l'insieme di punti è noto a priori prende solo confronti O (n * log n). Se si desidera aggiungere punti nel frattempo, è necessario tenerli in un set ordinato come un albero di ricerca binario bilanciato. In tal caso l'aggiunta di un nuovo punto richiede confronti O (log n) ed è esattamente lo stesso per la soluzione che coinvolge coordinate polari.
ciamej,

2
Manca questo caso: if (ax - center.x <0 && bx - center.x> = 0) restituisce false;
Tom Martin

2
Ehilà. È piuttosto vecchio, ma: "Questo ordinerà i punti in senso orario a partire dalle 12". Perché le 12 e come posso cambiarlo in 6? Qualcuno può dirmelo?
Ismoh,

20

Un approccio alternativo interessante al tuo problema sarebbe quello di trovare il minimo approssimativo al Problema del commesso viaggiatore (TSP), vale a dire. il percorso più breve che collega tutti i tuoi punti. Se i tuoi punti formano una forma convessa, dovrebbe essere la soluzione giusta, altrimenti dovrebbe comunque avere un bell'aspetto (una forma "solida" può essere definita come quella che ha un basso rapporto perimetro / area, che è ciò che stiamo ottimizzando qui) .

Puoi utilizzare qualsiasi implementazione di un ottimizzatore per il TSP, di cui sono abbastanza sicuro che puoi trovare un sacco nella tua lingua preferita.


Yikes. "Interessante" è un eufemismo. :)
Iteratore

@Iteratore: sono stato abbastanza contento della mia idea, sono rimasto piuttosto deluso dal fatto di essere votato a favore: - / Pensi che sia valido?
static_rtti

1
Stavo suggerendo di usare una delle molte approssimazioni veloci, non l'algoritmo originale NP-completo, ovviamente.
static_rtti,

6
Apprezzo l'angolo aggiuntivo! Avere diverse risposte valide, anche se molto diverse, potrebbe essere di grande aiuto se in futuro qualcuno dovesse imbattersi in questo thread alla ricerca di opzioni di brainstorming.
Philipp Lenssen,

1
Nota che il mio approccio è probabilmente più lento, ma più corretto in casi complessi: immagina il caso in cui i punti per un "8", per esempio. Le coordinate polari non ti aiuteranno in quel caso e il risultato che otterrai dipenderà fortemente dal centro che hai scelto. La soluzione TSP è indipendente da qualsiasi parametro "euristico".
static_rtti,

19

Quello che stai chiedendo è un sistema noto come coordinate polari . La conversione da coordinate cartesiane a coordinate polari avviene facilmente in qualsiasi lingua. Le formule sono disponibili in questa sezione .

Dopo la conversione in coordinate polari, basta ordinare per angolo, theta.


4
Funzionerà, ma avrà anche il difetto di fare più calcoli del necessario per rispondere alla domanda di ordinazione. In pratica, in realtà non ti preoccupi né degli angoli reali né delle distanze radiali, ma solo del loro ordine relativo. La soluzione di ciamej è migliore perché evita divisioni, radici quadrate e alberi.
RBerteig,

1
Non sono sicuro di quale sia il tuo criterio per "meglio". Ad esempio, confrontare tutti i punti tra loro è una specie di spreco di calcolo. Il Trig non è qualcosa che spaventa gli adulti, vero?
Iteratore

3
Non è che il grilletto sia spaventoso. Il problema è che il grilletto è costoso da calcolare e non era necessario per determinare l'ordine relativo degli angoli. Allo stesso modo, non è necessario prendere le radici quadrate per mettere in ordine i raggi. Una conversione completa da coordinate cartesiane a coordinate polari farà sia una radice tangente che una radice quadrata. Quindi la tua risposta è corretta, ma nel contesto della computer grafica o della geometria computazionale è probabile che non sia il modo migliore per farlo.
RBerteig,

Fatto. Tuttavia, l'OP non ha pubblicato come comp-geo, che era un tag di qualcun altro. Tuttavia, sembra che l'altra soluzione sia polinomiale nel numero di punti, o mi sbaglio? Se è così, questo brucia più cicli del grilletto.
Iteratore

In realtà non avevo notato il tag comp-geo, pensavo solo che le uniche applicazioni razionali per la domanda dovevano essere l'una o l'altra. Dopotutto, la domanda sulle prestazioni diventa discutibile se ci sono solo pochi punti e / o l'operazione verrà eseguita abbastanza raramente. A quel punto, sapere come farlo diventa importante ed è per questo che sono d'accordo che la tua risposta è corretta. Spiega come calcolare la nozione di "ordine in senso orario" in termini che possono essere spiegati praticamente a chiunque.
RBerteig,

3

Un'altra versione (restituisce vero se a viene prima di b in senso antiorario):

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

Questo è più veloce, perché il compilatore (testato su Visual C ++ 2015) non genera il salto per calcolare dax, day, dbx, dby. Qui l'assembly di output dal compilatore:

; 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

Godere.


1
Le due dichiarazioni di ritorno nello switch sono matematicamente equivalenti. C'è un motivo per avere l'interruttore?
unagi,

0
  • vector3 a = new vector3 (1, 0, 0) .............. wrt X_axis
  • vector3 b = any_point - Center;
- 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

Finalmente ottieni i verts ordinati Anticlockwize

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.