【问题标题】:What algorithm can I use to determine points within a semi-circle?我可以使用什么算法来确定半圆内的点?
【发布时间】:2010-10-06 19:20:52
【问题描述】:

我有一个二维点列表,我想获得它们中的哪些落在半圆内。

最初,目标形状是一个与 x 和 y 轴对齐的矩形。因此,当前算法通过 X 坐标和二进制搜索对可能落在矩形内的第一个对进行排序。然后它依次迭代每个点。当它碰到一个超出目标矩形的 X 和 Y 上限的位置时,它会停止。

这不适用于半圆,因为您无法为其确定有效的上/下 x 和 y 边界。半圆可以有任何方向。

最坏的情况,我会在半圆中找到一个维度(比如 x)的最小值,二进制搜索到超出它的第一个点,然后依次测试这些点,直到我超出那个上限方面。基本上是在网格上测试整个乐队的分数。问题是这最终会检查许多不在范围内的点。

【问题讨论】:

  • 这个问题可以用插图来说明。

标签: algorithm optimization geometry


【解决方案1】:

检查一个点是在半圆(或矩形)内部还是外部是一个常数时间操作。

检查 N 个点位于半圆或矩形的内部或外部是 O(N)。

对你的 N 个点进行排序是 O(N*lg(N))。

顺序测试所有点比排序然后基于二分搜索快速剔除点要快。

这可能是看起来快和快是两种不同事物的时代之一。

编辑

还有一种非常简单的方法可以测试半圆中某个点的包容性,而无需费力地进行旋转、变换等操作。

将半圆表示为两个分量:

  • 从点ab的线段代表半圆的直径
  • left-ofright-of 的方向表示半圆位于线段 ab 的左侧或右侧strong> 从 ab

您可以利用右手定则来确定该点是否在半圆内。

然后一些伪代码来测试点 p 是否在半圆中,例如:

procedure bool is_inside:
    radius = distance(a,b)/2
    center_pt = (a+b)/2    
    vec1 = b - center_pt
    vec2 = p - center_pt
    prod = cross_product(vec1,vec2) 
    if orientation == 'left-of'
        return prod.z >= 0 && distance(center_pt,p) <= radius
    else
        return prod.z <= 0 && distance(center_pt,p) <= radius

此方法的另一个好处是不使用任何三角函数,您可以通过与平方距离进行比较来消除所有平方根。您还可以通过缓存 'vec1' 计算、半径计算、center_pt 计算并重新排序一些操作以提早退出来加速它。但我试图弄清楚。

“cross_product”返回一个 (x,y,z) 值。它检查 z 分量是正还是负。这也可以通过不使用真正的叉积而只计算 z 分量来加速。

【讨论】:

  • 啊,有趣。我想这将永远是正确的,因为没有数据结构可以在少于线性的时间内维护它们。尝试减少我检查的点数真是太诱人了。
  • 布列塔尼在另一个答案中建议的空间分区可能会减少要检查的点。它仍将用另一种恒定时间操作代替;但是,我认为维护定位元数据将少于进行距离计算。
  • 但是请注意,如果您有很多半圆要测试,您仍然可以通过只排序一次然后进行二分搜索来获得渐近加速。
  • 这看起来不错,但我虽然知道交叉产品,但我不确定你的“z”组件是什么。
  • @Lance Roberts,叉积返回一个与其他 2 个向量正交的向量。因此,XY 平面中 2 个向量的叉积始终沿 z 轴,因为这是 2D,您可以将 vec1 和 vec2 视为 z 分量为 0。有关更多信息,请查看维基百科页面的叉积。
【解决方案2】:

如果您的点具有整数坐标,最快的解决方案可能是查找表。由于半圆是凸的,对于每个 y 坐标,您会得到一个固定范围的 x,因此查找表中的每个条目都会给出最大和最小 X 坐标。

当然,您仍然需要预先计算表格,如果您的半圆不固定,您可能会经常这样做。也就是说,这基本上是曾经渲染半圆的一部分——通过重复调用水平线绘制函数,整个形状将被渲染为一系列水平跨度。

首先要计算跨度(如果您需要重复计算),您可能需要查找 Michael Abrash 的图形编程禅的旧副本。这描述了 Bresenhams 著名的直线算法和不太知名的 Hardenburghs 圆算法。将两者的面向跨度的版本结合起来快速计算半圆的跨度应该不会太难。

IIRC,Hardenburgh 使用 x^2 + y^2 = 半径^2,但利用了您通过跨度来避免计算平方根这一事实 - 我认为它使用 (x+1)^ 的事实2 = x^2 + 2x + 1 和 (y-1)^2 = y^2 - 2y + 1,保持 x、y、x^2 和 (radius^2 - y^2) 的运行值,所以每个step 只需要比较(当前 x^2 + y^2 是否太大)和一些添加。它只针对一个八分圆(确保单像素步长的唯一方法),并通过对称扩展到整个圆。

一旦你有了整个圆圈的跨度,应该很容易使用 Bresenhams 来切断你不想要的一半。

当然,只有在您完全确定自己需要(并且可以使用整数)的情况下,您才会这样做。否则坚持使用stbuton。

【讨论】:

    【解决方案3】:

    看起来一个简单的方案在这里可以工作。

    1. 通过首先计算凸包来减少集合中的点数。只有凸包上的点将有助于与任何凸边界形状的任何交互。所以只保留船体周边的点子集。

    2. 可以很容易地认为,最小半径边界半圆必须有一个凸包的边缘(两个点)沿半圆的直径重合。即,如果船体的某些边缘不在直径范围内,则存在一个直径较小的不同半圆,其中包含相同的点集。

    3. 依次测试每条边。 (凸包通常有相对较少的边,所以这会很快。)现在它变成了一个简单的一维最小化问题。如果我们选择假设所讨论的边缘位于直径上,那么我们只需要找到球的中心。它必须位于我们认为是直径的当前线。因此,作为该点沿当前直径的位置的函数,只需找到离标称中心最远的点。通过最小化该距离,我们找到沿该线的最小半圆的半径作为直径。

    现在,只需选择在凸包所有边缘上找到的最佳半圆。

    【讨论】:

    • 我上传了一个工具来完成这些计算,用 matlab 编写。 mathworks.com/matlabcentral/fileexchange/23502 如果你不使用matlab,代码有据可查,仍然可以作为伪代码来构建你自己的。它对大型数据集非常有效。
    【解决方案4】:

    这是我编写的函数的一部分,它确实为基于图块的游戏中的武器提供锥形发射弧。

    float lineLength;
    float lineAngle;
    for(int i = centerX - maxRange; i < centerX + maxRange + 1; i++){
        if(i < 0){
            continue;
        }
        for(int j = centerY -  maxRange; j < centerY + maxRange + 1; j++){
            if(j < 0){
                continue;
            }
    
            lineLength = sqrt( (float)((centerX - i)*(centerX - i)) + (float)((centerY - j)*(centerY - j)));
            lineAngle = lineAngles(centerX, centerY, forwardX, forwardY, centerX, centerY, i, j);
    
            if(lineLength < (float)maxRange){
                if(lineAngle < arcAngle){
                    if( (float)minRange <= lineLength){ 
                        AddToHighlightedTiles(i,j);
                    }
                }
            }
        }
    }
    

    变量应该是不言自明的,线角度函数需要 2 条线并找到它们之间的角度。根据您指向的角度,forwardX 和 forwardY 只是从中心 X 和 Y 沿正确方向的一个图块。这些可以通过 switch 语句轻松获得。

    float lineAngles(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4){
        int a = x2 - x1;
        int b = y2 - y1;
        int c = x4 - x3;
        int d = y4 - y3;
    
        float ohSnap = ( (a * c) + (b * d) )/(sqrt((float)a*a + b*b) * sqrt((float)c*c + d*d) );
        return acos(ohSnap) * 180 / 3.1415926545f;
    }
    

    【讨论】:

    • 您可以在不使用任何三角函数的情况下,通过使用叉积来比较角度。还将长度比较的两侧平方,以消除昂贵的sqrt 调用。
    【解决方案5】:

    执行此操作的最快方法取决于您的典型数据。如果您要查看真实世界的数据,请先执行此操作。当点在半圆之外时,通常是因为它们在圆之外吗?你的半圆形通常是薄饼片吗?

    有几种方法可以用向量来做到这一点。您可以将圆缩放为单位圆并使用叉积并查看结果向量。您可以使用点积并查看预期点如何落在其他向量上。

    如果你想要一些非常容易理解的东西,首先检查以确保它在圆内,然后获取角度并确保它在决定半圆的两个向量的角度之间。


    编辑:我忘记了半圆总是半圆。我在想一个圆的任意部分。

    现在我已经记住了半圆是什么,这就是我的做法。类似于stbuton的解法,只是表示半圆的方式不同。

    我将半圆表示为平分半圆的单位向量。您可以通过交换 x 和 y 并取反其中一个,轻松地从指示半圆边界的任一向量(因为它们与表示相距 90 度)中得到它。

    现在您只需穿过从圆心减去要测试的点创建的向量。 z的符号告诉你点是否在半圆内,z的长度可以和半径比较。

    我完成了 Cool Pool 的所有物理(来自 Sierra Online)。这一切都是在向量中完成的,并且充满了点和十字。向量解决方案很快。 Cool Pool 能够在 P60 上运行,并且进行了合理的休息甚至旋转。

    注意:对于检查 sqrt(xx+yy) 的解决方案,甚至不要执行 sqrt。相反,保持半径的平方并与之进行比较。

    【讨论】:

      【解决方案6】:

      我猜有人在这里找到了与我相同的解决方案,但我没有代码可以显示,因为它在我的记忆中相当遥远......

      我会按步骤进行...
      1. 如果我在一个圆圈内,我会查看...如果是,请查看圆圈的哪一侧。
      2. 通过绘制一个来自半球体向量的法线向量。我可以知道我是在矢量的后面还是前面……如果你知道哪一边是半球,哪一边是空洞……如果你在矢量之内,那将非常容易找到半球形。你必须做点积。

      我不确定它是否足够清楚,但测试应该不会那么难......最后你必须寻找一个负值或正值......如果它是 0 你在半球的向量,所以由你决定它是在半球的外面还是里面。

      【讨论】:

        【解决方案7】:
        • 将圆弧的中心平移到原点
        • 计算纵坐标轴与半圆半径端点的夹角
        • 用相同的 dx,dy 翻译问题点
        • 计算从原点到平移 x,y 点的距离,如果 d > 圆半径/圆弧消除

          • 计算纵坐标轴和终点之间的角度
          • 如果角度不在半圆的起点和终点弧之间,则消除

          剩余点应在半圆内

        【讨论】:

          【解决方案8】:

          您可以在一个圆中找到点,也可以在给定坡度的一侧找到点,对吧?

          只需将两者结合起来。

          【讨论】:

            【解决方案9】:

            “也许他们可以暴力破解,因为他们有专用的完整 GPU。”

            如果您有 GPU 供您使用,那么还有更多方法可以做到这一点。例如,使用模板缓冲区:

            • 清除模板缓冲区并将模板操作设置为递增
            • 将半圆渲染到模板缓冲区
            • 提供积分
            • 读回像素并检查您的点的值
            • 半圆内的点会增加两次。

            This article 描述了如何在 OpenGL 中使用模板缓冲区。

            【讨论】:

              【解决方案10】:

              如果有执行此操作的标准算法,我相信其他人会想出它,但如果没有:您可以尝试按距圆心的距离对点进行排序,并仅迭代距离为小于半圆的半径。或者,如果计算距离很昂贵,我会尝试找到半圆的边界框(甚至是半圆所在的圆的边界正方形)并迭代该范围内的点。在某种程度上它取决于点的分布,即您希望它们中的大多数还是只有一小部分落在半圆内?

              【讨论】:

                【解决方案11】:

                首先,平移和旋转半圆,使一端在负 X 轴上,另一端在正 X 轴上,以原点为中心(当然,您不会实际平移并旋转它,您将获得适当的数字来平移和旋转它,并在下一步中使用它们)。

                然后,您可以将其视为一个圆,忽略所有负 y 值,只使用 X 和 Y 的平方和的平方根进行测试,看看它是否小于或等于半径。

                【讨论】:

                • 如果我这样做,有没有办法避免测试整个点集?我想图形引擎必须做类似的事情来确定玩家“看到”什么(或必须渲染什么)。也许他们可以强行使用它,因为他们有专用的完整 GPU。
                • 第一个简单的步骤是消除 X 或 Y >= 到半径的所有点。
                • 如果你真的很想避免测试整个点集,你可以尝试空间分区,并消除不符合标准的点的整个网格单元 - 只检查可能的网格单元包含你的观点。
                • 另外,在实现的时候,不要每次都取平方根,只要比较平方和与半径的平方即可。
                猜你喜欢
                • 2011-04-25
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2021-11-09
                • 2013-12-16
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多