Сортировать точки по часовой стрелке?

156

Учитывая массив точек x, y, как мне отсортировать точки этого массива по часовой стрелке (вокруг их средней средней точки)? Моя цель состоит в том, чтобы передать точки в функцию создания линий, чтобы в итоге получилось нечто «сплошное», настолько выпуклое, насколько это возможно, без пересекающихся линий.

Для чего это стоит, я использую Lua, но любой псевдокод был бы оценен.

Обновление: для справки, это код Lua, основанный на превосходном ответе Ciamej (игнорируйте мой префикс "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
Подумайте о том, чтобы вычислить угол радиальной линии, проходящей через эту точку. Затем сортируйте по углу.
Президент Джеймс К. Полк
Если вы не знали, у lua есть встроенная функция, ipairs(tbl)которая перебирает индексы и значения tbl от 1 до #tbl. Таким образом, для расчета суммы вы можете сделать это, что большинство людей считает чище:for _, p in ipairs(points) do pointsSum.x = pointsSum.x + p.x; pointsSum.y = pointsSum.y + p.y end
Ponkadoodle
2
@Wallacoloo Это весьма спорно. Кроме того, в ванильном Lua ipairsзначительно медленнее, чем числовой для цикла.
Александр Гладыш
Мне пришлось внести небольшие изменения, чтобы заставить его работать для моего случая (просто сравнивая две точки относительно центра). gist.github.com/personalnadir/6624172 Все эти сравнения с 0 в коде, кажется, предполагают, что точки распределены вокруг начала координат, а не произвольной точки. Я также думаю, что первое условие будет сортировать точки ниже центральной точки неправильно. Спасибо за код, хотя, это было действительно полезно!
personalnadir

Ответы:

192

Сначала вычислите центральную точку. Затем сортируйте точки, используя любой алгоритм сортировки, который вам нравится, но используйте специальную процедуру сравнения, чтобы определить, меньше ли одна точка, чем другая.

Вы можете проверить, находится ли одна точка (а) слева или справа от другой (б) относительно центра, с помощью простого вычисления:

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

если результат равен нулю, то они находятся на одной линии от центра, если он положительный или отрицательный, то он находится на одной или другой стороне, поэтому одна точка будет предшествовать другой. Используя его, вы можете построить отношение меньше чем, чтобы сравнить точки и определить порядок, в котором они должны появляться в отсортированном массиве. Но вы должны определить, где находится начало этого порядка, я имею в виду, какой угол будет начальным (например, положительная половина оси X).

Код для функции сравнения может выглядеть так:

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

Это упорядочит точки по часовой стрелке, начиная с 12 часов. Очки в тот же «час» будут заказываться, начиная с тех, которые находятся дальше от центра.

Если вы используете целочисленные типы (которых нет в Lua), вам нужно убедиться, что переменные det, d1 и d2 относятся к типу, который сможет содержать результат выполненных вычислений.

Если вы хотите добиться чего-то твердого, выпуклого, насколько это возможно, то я думаю, что вы ищете выпуклый корпус . Вы можете вычислить его с помощью сканирования Грэма . В этом алгоритме вы также должны сортировать точки по часовой стрелке (или против часовой стрелки), начиная со специальной точки поворота. Затем вы повторяете простые шаги цикла каждый раз, проверяя, поворачиваете ли вы влево или вправо, добавляя новые точки к выпуклой оболочке, эта проверка основана на перекрестном произведении, как в приведенной выше функции сравнения.

Редактировать:

Добавлен еще один оператор if, if (a.y - center.y >= 0 || b.y - center.y >=0)чтобы убедиться, что точки с x = 0 и отрицательным y отсортированы, начиная с тех, которые находятся дальше от центра. Если вас не волнует порядок точек в один и тот же «час», вы можете опустить это выражение if и всегда возвращать a.y > b.y.

Исправлены первые операторы if с добавлением -center.xи-center.y .

Добавлен второй оператор if (a.x - center.x < 0 && b.x - center.x >= 0). Это было очевидное упущение, что оно отсутствовало. Операторы if могут быть реорганизованы сейчас, потому что некоторые проверки избыточны. Например, если первое условие в первом операторе if ложно, то первое условие второго условия if должно быть истинным. Однако я решил оставить код таким, какой он есть, ради простоты. Вполне возможно, что компилятор все равно оптимизирует код и даст тот же результат.

ciamej
источник
25
+1: нет atan(), нет квадратного корня и даже нет делений. Это хороший пример мышления компьютерной графики. Отберите все простые случаи как можно скорее, и даже в сложных случаях рассчитайте как можно меньше, чтобы узнать требуемый ответ.
RBerteig
Но это требует сравнения всех точек со всеми остальными. Есть ли простой способ вставки новых точек?
Итератор
2
если набор точек известен априори, то требуется только O (n * log n) сравнений. Если вы хотите добавить точки в это время, вам нужно хранить их в отсортированном наборе, таком как сбалансированное двоичное дерево поиска. В таком случае добавление новой точки требует O (log n) сравнений, и это точно так же для решения, включающего полярные координаты.
ciamej
2
Это отсутствует в случае: if (ax - center.x <0 && bx - center.x> = 0) вернуть false;
Том Мартин
2
Привет всем. Он довольно старый, но: «Это позволит упорядочить точки по часовой стрелке, начиная с 12 часов». Почему 12 часов и как я могу изменить его на 6? Кто-нибудь может сказать мне?
Исмо
20

Интересным альтернативным подходом к вашей проблеме было бы найти приблизительный минимум для задачи коммивояжера (TSP), т.е. кратчайший маршрут, связывающий все ваши точки. Если ваши точки образуют выпуклую форму, это должно быть правильным решением, в противном случае они все равно должны хорошо выглядеть («сплошная» форма может быть определена как форма с низким отношением периметр / площадь, что мы и оптимизируем здесь) ,

Вы можете использовать любую реализацию оптимизатора для TSP, и я уверен, что вы сможете найти тонну на вашем языке.

static_rtti
источник
Хлоп. «Интересно» - это занижение. :)
Итератор
@Iterator: Я был вполне доволен своей идеей, я был очень разочарован тем, что за нее проголосовали: - / Как вы думаете, она действительна?
static_rtti
1
Я предлагал использовать одно из многих быстрых приближений, а не оригинальный алгоритм, конечно, полный NP.
static_rtti
6
Я ценю дополнительный угол! Иметь несколько правильных, хотя и очень разных ответов, может быть очень полезно, если кто-то в будущем случайно наткнется на эту тему в поисках вариантов мозгового штурма.
Филипп Ленссен
1
Обратите внимание, что мой подход, вероятно, медленнее, но более корректен в сложных случаях: представьте случай, когда точки для «8», например. Полярные координаты не помогут вам в этом случае, и результат, который вы получите, будет сильно зависеть от выбранного вами центра. Решение TSP не зависит от каких-либо «эвристических» параметров.
static_rtti
19

То, что вы просите, это система, известная как полярные координаты . Преобразование из декартовых в полярные координаты легко выполняется на любом языке. Формулы можно найти в этом разделе .

После преобразования в полярные координаты, просто отсортируйте по углу, тета.

Итератор
источник
4
Это будет работать, но у него также будет недостаток в выполнении большего количества вычислений, чем необходимо для ответа на вопрос об упорядочении. На практике вас не интересуют ни фактические углы, ни радиальные расстояния, а только их относительный порядок. Решение ciamej лучше, потому что оно избегает делений, квадратных корней и триггеров.
RBerteig
1
Я не уверен, каков твой критерий для «лучше». Например, сравнение всех точек друг с другом является своего рода пустой тратой вычислений. Триг не то, что пугает взрослых, не так ли?
Итератор
3
Дело не в том, что триг это страшно. Проблема состоит в том, что триггер является дорогостоящим для вычисления и не нужен для определения относительного порядка углов. Точно так же вам не нужно брать квадратные корни, чтобы привести радиусы в порядок. Полное преобразование из декартовых в полярные координаты будет выполнять как арктангенс, так и квадратный корень. Следовательно, ваш ответ правильный, но в контексте компьютерной графики или вычислительной геометрии это, вероятно, не лучший способ сделать это.
RBerteig
Понял. Тем не менее, OP не публиковал как comp-geo, это был тег от кого-то еще. Тем не менее, похоже, что другое решение является полиномиальным по количеству точек, или я ошибаюсь? Если это так, это сжигает больше циклов, чем триггер.
Итератор
На самом деле я не заметил тега comp-geo, я просто предположил, что единственными рациональными приложениями для этого вопроса должны быть одно или другое. В конце концов, вопрос производительности становится спорным, если есть только несколько точек, и / или операция будет выполняться достаточно редко. В этот момент важно знать, как это сделать, и поэтому я согласен, что ваш ответ верен. Он объясняет, как вычислить понятие «порядок по часовой стрелке» в терминах, которые могут быть объяснены практически любому.
RBerteig
3

Другая версия (верните true, если a предшествует b в направлении против часовой стрелки):

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

Это быстрее, потому что компилятор (протестированный на Visual C ++ 2015) не генерирует переход для вычисления dax, day, dbx, dby. Вот выходная сборка от компилятора:

; 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

Наслаждаться.

AGPX
источник
1
Два оператора возврата в переключателе математически эквивалентны. Есть ли причина наличия переключателя?
Унаги
0
  • vector3 a = новый вектор3 (1, 0, 0) .............. относительно X_axis
  • vector3 b = любая_точка - Центр;
- 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

Наконец, вы получите Anticlockwize отсортированные верты

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

павана
источник