¿Ordenar puntos en el sentido de las agujas del reloj?

156

Dada una matriz de puntos x, y, ¿cómo clasifico los puntos de esta matriz en el sentido de las agujas del reloj (alrededor de su punto central promedio general)? Mi objetivo es pasar los puntos a una función de creación de línea para terminar con algo que parece bastante "sólido", lo más convexo posible sin líneas que se crucen.

Por lo que vale, estoy usando Lua, pero cualquier pseudocódigo sería apreciado.

Actualización: Como referencia, este es el código Lua basado en la excelente respuesta de Ciamej (ignore mi prefijo de "aplicación"):

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
fuente
1
Piensa en calcular el ángulo de la línea radial a través de ese punto. Luego ordenar por ángulo.
Presidente James K. Polk
En caso de que no lo supiera, lua tiene una función integrada ipairs(tbl)que itera sobre los índices y valores de tbl de 1 a #tbl. Así que para el cálculo de la suma, se puede hacer esto, que la mayoría de la gente encuentra apariencia más limpia:for _, p in ipairs(points) do pointsSum.x = pointsSum.x + p.x; pointsSum.y = pointsSum.y + p.y end
Ponkadoodle
2
@Wallacoloo Eso es muy discutible. Además, en vainilla, Lua ipairses significativamente más lento que el numérico para el bucle.
Alexander Gladysh
Tuve que hacer algunos pequeños cambios para que funcione para mi caso (solo comparando dos puntos en relación con un centro). gist.github.com/personalnadir/6624172 Todas esas comparaciones a 0 en el código parecen suponer que los puntos se distribuyen alrededor del origen, en oposición a un punto arbitrario. También creo que la primera condición ordenará los puntos debajo del punto central de forma incorrecta. Sin embargo, gracias por el código, ¡ha sido realmente útil!
personalnadir

Respuestas:

192

Primero, calcule el punto central. Luego clasifique los puntos utilizando el algoritmo de clasificación que desee, pero utilice una rutina de comparación especial para determinar si un punto es menor que el otro.

Puede verificar si un punto (a) está a la izquierda o a la derecha del otro (b) en relación con el centro mediante este simple cálculo:

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

si el resultado es cero, entonces están en la misma línea desde el centro, si es positivo o negativo, entonces está de un lado o del otro, por lo que un punto precederá al otro. Al usarlo, puede construir una relación menor que para comparar puntos y determinar el orden en que deben aparecer en la matriz ordenada. Pero tiene que definir dónde está el comienzo de ese orden, quiero decir qué ángulo será el inicial (por ejemplo, la mitad positiva del eje x).

El código para la función de comparación puede verse así:

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

Esto ordenará los puntos en sentido horario a partir de las 12 en punto. Los puntos en la misma "hora" se ordenarán a partir de los que están más lejos del centro.

Si usa tipos enteros (que no están realmente presentes en Lua), debería asegurarse de que las variables det, d1 y d2 sean de un tipo que pueda contener el resultado de los cálculos realizados.

Si desea lograr algo que parezca sólido, lo más convexo posible, entonces supongo que está buscando un casco convexo . Puede calcularlo con Graham Scan . En este algoritmo, también debe ordenar los puntos en el sentido de las agujas del reloj (o en sentido contrario) a partir de un punto de giro especial. Luego repite pasos de bucle simples cada vez que verifica si gira a la izquierda o derecha agregando nuevos puntos al casco convexo, esta verificación se basa en un producto cruzado al igual que en la función de comparación anterior.

Editar:

Se agregó una instrucción if más if (a.y - center.y >= 0 || b.y - center.y >=0)para asegurarse de que los puntos que tienen x = 0 y y negativo estén ordenados a partir de los que están más alejados del centro. Si no le importa el orden de los puntos en la misma 'hora', puede omitir esto si la declaración y siempre regresa a.y > b.y.

Se corrigieron las primeras declaraciones if con suma -center.xy -center.y.

Se agregó la segunda declaración if (a.x - center.x < 0 && b.x - center.x >= 0). Era un descuido obvio que faltaba. Las declaraciones if podrían reorganizarse ahora porque algunas verificaciones son redundantes. Por ejemplo, si la primera condición en la primera instrucción if es falsa, entonces la primera condición de la segunda if debe ser verdadera. Decidí, sin embargo, dejar el código como es por simplicidad. Es muy posible que el compilador optimice el código y produzca el mismo resultado de todos modos.

ciamej
fuente
25
+1: No atan(), sin raíz cuadrada, e incluso sin divisiones. Este es un buen ejemplo del pensamiento gráfico por computadora. Elimine todos los casos fáciles lo antes posible, e incluso en los casos difíciles, calcule lo menos posible para conocer la respuesta requerida.
RBerteig
Pero requiere comparar todos los puntos con todos los demás. ¿Existe un método simple para insertar nuevos puntos?
Iterator
2
si el conjunto de puntos se conoce a priori, solo se necesitan comparaciones O (n * log n). Si desea agregar puntos mientras tanto, debe mantenerlos en un conjunto ordenado, como un árbol de búsqueda binario equilibrado. En tal caso, agregar un nuevo punto requiere comparaciones O (log n) y es exactamente lo mismo para la solución que involucra coordenadas polares.
ciamej
2
¿Falta este caso? If (ax - center.x <0 && bx - center.x> = 0) devuelve falso;
Tom Martin
2
Hola. Es bastante antiguo, pero: "Esto ordenará los puntos en el sentido de las agujas del reloj a partir de las 12 en punto". ¿Por qué las 12 en punto y cómo puedo cambiarlo a 6? ¿Alguien me puede decir?
Ismoh
20

Un enfoque alternativo interesante para su problema sería encontrar el mínimo aproximado para el Problema del vendedor ambulante (TSP), es decir. La ruta más corta que une todos sus puntos. Si sus puntos forman una forma convexa, debería ser la solución correcta, de lo contrario, debería verse bien (una forma "sólida" puede definirse como una que tiene una baja relación perímetro / área, que es lo que estamos optimizando aquí) .

Puede usar cualquier implementación de un optimizador para el TSP, del cual estoy bastante seguro de que puede encontrar una tonelada en el idioma que elija.

static_rtti
fuente
Yikes "Interesante" es un eufemismo. :)
Iterator
@Iterator: Estaba bastante contento con mi idea, me decepcionó mucho que me votaran por ella: - / ¿Crees que es válida?
static_rtti
1
Estaba sugiriendo utilizar una de las muchas aproximaciones rápidas, no el algoritmo original NP-complete, por supuesto.
static_rtti
66
Agradezco el ángulo adicional! Tener varias respuestas válidas, aunque muy diferentes, podría ser de gran ayuda si alguien en el futuro se topa con este hilo buscando opciones de lluvia de ideas.
Philipp Lenssen
1
Tenga en cuenta que mi enfoque es probablemente más lento, pero más correcto en casos complejos: imagine el caso donde los puntos para un "8", por ejemplo. Las coordenadas polares no te ayudarán en ese caso, y el resultado que obtendrás dependerá en gran medida del centro que elijas. La solución TSP es independiente de cualquier parámetro "heurístico".
static_rtti
19

Lo que estás pidiendo es un sistema conocido como coordenadas polares . La conversión de coordenadas cartesianas a polares se realiza fácilmente en cualquier idioma. Las fórmulas se pueden encontrar en esta sección .

Después de convertir a coordenadas polares, solo ordena por el ángulo, theta.

Iterador
fuente
44
Esto funcionará, pero también tendrá el defecto de hacer más cálculos de los necesarios para responder la pregunta de pedido. En la práctica, en realidad no te importan los ángulos reales o las distancias radiales, solo su orden relativo. La solución de ciamej es mejor porque evita divisiones, raíces cuadradas y trigonometría.
RBerteig
1
No estoy seguro de cuál es su criterio para "mejor". Por ejemplo, comparar todos los puntos entre sí es un desperdicio de cálculo. Trig no es algo que asusta a los adultos, ¿verdad?
Iterator
3
No es que el trigonometro da miedo. El problema es que el cálculo de trigonometría es costoso y no fue necesario para determinar el orden relativo de los ángulos. Del mismo modo, no necesita tomar las raíces cuadradas para ordenar los radios. Una conversión completa de coordenadas cartesianas a polares hará tanto una tangente de arco como una raíz cuadrada. Por lo tanto, su respuesta es correcta, pero en el contexto de los gráficos por computadora o la geometría computacional, es probable que no sea la mejor manera de hacerlo.
RBerteig
Entendido. Sin embargo, el OP no se publicó como comp-geo, esa fue una etiqueta de otra persona. Aún así, parece que la otra solución es polinómica en el número de puntos, ¿o me equivoco? Si es así, eso quema más ciclos que trigonometría.
Iterator
En realidad no había notado la etiqueta comp-geo, simplemente asumí que las únicas aplicaciones racionales para la pregunta tenían que ser una u otra. Después de todo, la cuestión del rendimiento se vuelve discutible si solo hay unos pocos puntos, y / o la operación se realizará raramente. En ese momento, saber cómo hacerlo se vuelve importante y es por eso que estoy de acuerdo en que su respuesta es correcta. Explica cómo calcular la noción de un "orden en el sentido de las agujas del reloj" en términos que se pueden explicar a casi cualquier persona.
RBerteig
3

Otra versión (devuelve verdadero si a viene antes de b en sentido antihorario):

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

Esto es más rápido, porque el compilador (probado en Visual C ++ 2015) no genera salto para calcular dax, day, dbx, dby. Aquí el ensamblaje de salida del compilador:

; 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

Disfrutar.

AGPX
fuente
1
Las dos declaraciones de retorno en el interruptor son matemáticamente equivalentes. ¿Hay alguna razón para tener el interruptor?
unagi
0
  • vector3 a = nuevo vector3 (1, 0, 0) .............. wrt X_axis
  • vector3 b = any_point - Centro;
- 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 obtienes Vert ordenados Anticlockwize

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

Pavana
fuente