按顺时针顺序对点进行排序?

Posted

技术标签:

【中文标题】按顺时针顺序对点进行排序?【英文标题】:Sort points in clockwise order? 【发布时间】:2011-10-22 18:40:18 【问题描述】:

给定一个 x,y 点数组,我如何按顺时针顺序(围绕它们的整体平均中心点)对该数组的点进行排序?我的目标是将这些点传递给一个线创建函数,最终得到一个看起来相当“实心”的东西,尽可能凸出,没有相交的线。

对于它的价值,我使用的是 Lua,但任何伪代码都会受到赞赏。

更新:作为参考,这是基于 Ciamej 出色答案的 Lua 代码(忽略我的“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

【问题讨论】:

考虑计算通过该点的径向线的角度。然后按角度排序。 如果你不知道,lua 有一个内置函数ipairs(tbl),它遍历从 1 到 #tbl 的 tbl 的索引和值。所以对于总和计算,你可以这样做,大多数人觉得它看起来更干净:for _, p in ipairs(points) do pointsSum.x = pointsSum.x + p.x; pointsSum.y = pointsSum.y + p.y end @Wallacoloo 这很有争议。此外,在原版 Lua 中,ipairs 比数字 for 循环慢得多。 我必须做一些小改动才能让它适用于我的情况(只是比较相对于中心的两个点)。 gist.github.com/personalnadir/6624172 代码中所有与 0 的比较似乎都假设这些点分布在原点周围,而不是任意点。我还认为第一个条件会错误地对中心点以下的点进行排序。不过感谢您的代码,它真的很有帮助! 【参考方案1】:

首先,计算中心点。 然后使用您喜欢的任何排序算法对点进行排序,但使用特殊的比较例程来确定一个点是否小于另一个点。

您可以通过这个简单的计算来检查一个点 (a) 相对于中心是在另一个 (b) 的左侧还是右侧:

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 变量属于能够保存执行计算结果的类型。

如果您想获得看起来坚固、尽可能凸的东西,那么我猜您正在寻找Convex Hull。您可以使用Graham Scan 计算它。 在此算法中,您还必须从特殊的枢轴点开始顺时针(或逆时针)对点进行排序。然后,每次检查是否向左或向右向凸包添加新点时重复简单的循环步骤,此检查基于叉积,就像上面的比较函数一样。

编辑:

添加了一个 if 语句 if (a.y - center.y &gt;= 0 || b.y - center.y &gt;=0) 以确保具有 x=0 和负 y 的点从离中心较远的点开始排序。如果您不关心同一“小时”的点数顺序,您可以省略此 if 语句并始终返回 a.y &gt; b.y

通过添加 -center.x-center.y 更正了第一个 if 语句。

添加了第二个 if 语句 (a.x - center.x &lt; 0 &amp;&amp; b.x - center.x &gt;= 0)。这是一个明显的疏忽,它丢失了。 if 语句现在可以重新组织,因为一些检查是多余的。例如,如果第一个 if 语句中的第一个条件为假,那么第二个 if 的第一个条件必须为真。但是,为了简单起见,我决定保留代码不变。编译器很有可能会优化代码并产生相同的结果。

【讨论】:

+1:没有atan(),没有平方根,甚至没有除法。这是计算机图形思维的一个很好的例子。尽快剔除所有简单的情况,即使在困难的情况下,也要尽可能少地计算以知道所需的答案。 如果点集是先验已知的,则只需进行 O(n*log n) 次比较。如果同时要添加点,则需要将它们保存在排序集中,例如平衡二叉搜索树。在这种情况下,添加一个新点需要 O(log n) 比较,这对于涉及极坐标的解决方案完全相同。 这是不是漏掉了这种情况:if (a.x - center.x = 0) return false; 你好。它很老了,但是:“这将从 12 点开始顺时针排列点。”为什么是 12 点,如何将其更改为 6?谁能告诉我? 对于寻找更多信息的人来说,这使用向量之间的叉积。【参考方案2】:

您需要的是一个称为polar coordinates 的系统。从笛卡尔坐标到极坐标的转换可以用任何语言轻松完成。公式可以在this section找到。

转换为极坐标后,只需按角度theta排序即可。

【讨论】:

这会起作用,但它也有缺陷,即执行的计算量超过了回答排序问题所需的计算量。在实践中,您实际上并不关心实际角度或径向距离,只关心它们的相对顺序。 ciamej 的解决方案更好,因为它避免了除法、平方根和三角函数。 我不确定您的“更好”标准是什么。例如,将所有点相互比较是一种计算浪费。 Trig 不会吓到成年人吧? 并不是说三叉戟是可怕的。问题是 trig 的计算成本很高,并且不需要确定角度的相对顺序。同样,您不需要取平方根来排列半径。从笛卡尔坐标到极坐标的完全转换将同时进行反正切和平方根。因此,您的答案是正确的,但在计算机图形学或计算几何学的背景下,它可能不是最佳方法 知道了。但是,OP 没有发布为 comp-geo,这是其他人的标签。尽管如此,看起来另一个解决方案是点数中的多项式,还是我弄错了?如果是这样,那会比 trig 消耗更多的周期。 我实际上并没有注意到 comp-geo 标签,我只是假设该问题的唯一合理应用必须是其中一个。毕竟,如果只有几个点,性能问题就会变得没有意义,和/或操作将很少完成。到那时,知道如何去做就变得很重要,这就是为什么我同意你的答案是正确的。它解释了如何用几乎任何人都可以解释的术语来计算“顺时针顺序”的概念。【参考方案3】:

解决您的问题的一个有趣的替代方法是找到旅行商问题 (TSP) 的近似最小值,即。连接所有点的最短路线。如果您的点形成凸形,它应该是正确的解决方案,否则,它应该看起来仍然不错(“实心”形状可以定义为具有低周长/面积比的形状,这是我们在这里优化的) .

您可以为 TSP 使用任何优化器的实现,我很确定您可以在其中找到大量您选择的语言。

【讨论】:

哎呀。 “有趣”是轻描淡写。 :) @Iterator:我对我的想法很满意,但被否决让我很失望:-/你认为它有效吗? 我建议使用许多快速近似中的一种,当然,不是 NP 完全原始算法。 我很欣赏额外的角度!如果将来有人碰巧偶然发现此线程以集思广益,那么如果有几个有效的但非常不同的答案可能会很有帮助。 请注意,我的方法可能更慢,但在复杂情况下更正确:例如,想象一下“8”点的情况。在这种情况下,极坐标对您没有帮助,您将获得的结果将在很大程度上取决于您选择的中心。 TSP 解决方案独立于任何“启发式”参数。【参考方案4】:

另一个版本(如果 a 逆时针方向在 b 之前,则返回 true):

    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

享受吧。

【讨论】:

switch 中的两个 return 语句在数学上是等价的。有开关的理由吗?【参考方案5】: vector3 a = new vector3(1 , 0 , 0).......w.r.t X_axis vector3 b = any_point - 中心;
- 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

【讨论】:

【参考方案6】:

这是一种按顺时针顺序对矩形顶点进行排序的方法。我修改了pyimagesearch提供的原方案,去掉了scipy依赖。

import numpy as np

def pointwise_distance(pts1, pts2):
    """Calculates the distance between pairs of points

    Args:
        pts1 (np.ndarray): array of form [[x1, y1], [x2, y2], ...]
        pts2 (np.ndarray): array of form [[x1, y1], [x2, y2], ...]

    Returns:
        np.array: distances between corresponding points
    """
    dist = np.sqrt(np.sum((pts1 - pts2)**2, axis=1))
    return dist

def order_points(pts):
    """Orders points in form [top left, top right, bottom right, bottom left].
    Source: https://www.pyimagesearch.com/2016/03/21/ordering-coordinates-clockwise-with-python-and-opencv/

    Args:
        pts (np.ndarray): list of points of form [[x1, y1], [x2, y2], [x3, y3], [x4, y4]]

    Returns:
        [type]: [description]
    """
    # sort the points based on their x-coordinates
    x_sorted = pts[np.argsort(pts[:, 0]), :]

    # grab the left-most and right-most points from the sorted
    # x-roodinate points
    left_most = x_sorted[:2, :]
    right_most = x_sorted[2:, :]

    # now, sort the left-most coordinates according to their
    # y-coordinates so we can grab the top-left and bottom-left
    # points, respectively
    left_most = left_most[np.argsort(left_most[:, 1]), :]
    tl, bl = left_most

    # now that we have the top-left coordinate, use it as an
    # anchor to calculate the Euclidean distance between the
    # top-left and right-most points; by the Pythagorean
    # theorem, the point with the largest distance will be
    # our bottom-right point. Note: this is a valid assumption because
    # we are dealing with rectangles only.
    # We need to use this instead of just using min/max to handle the case where
    # there are points that have the same x or y value.
    D = pointwise_distance(np.vstack([tl, tl]), right_most)
    
    br, tr = right_most[np.argsort(D)[::-1], :]

    # return the coordinates in top-left, top-right,
    # bottom-right, and bottom-left order
    return np.array([tl, tr, br, bl], dtype="float32")

【讨论】:

以上是关于按顺时针顺序对点进行排序?的主要内容,如果未能解决你的问题,请参考以下文章

按给定轴的角度对点进行排序?

如何逆时针排序具有n个元素的一个向量的所有点

计算几何多边形点集排序

Scrambled Polygon---poj2007(利用叉积排序)

Drop VoicingLIS

用Java对点列表进行排序[重复]