【问题标题】:Efficiently find points inside a circle sector有效地找到圆形扇区内的点
【发布时间】:2012-11-19 02:30:50
【问题描述】:

我有一组随机分布的二维点。我需要对这些点的一小部分执行时间密集型操作,但我需要首先弄清楚我需要在哪些点上执行这种时间密集型操作。要确定我需要哪些点,它们必须通过一系列几何标准。

最基本的标准是它们在特定点的一定距离内。第二个最基本的标准是它们是否包含在从该特定点向外延伸的圆形扇区(二维圆锥)内。 (编辑:定期调用此操作,每次使用不同的特定点但相同的 2d 点集。)

我最初的想法是创建一个包含 2d 点的网格,然后沿着圆锥体进行迭代,抓取与之相交的网格正方形。根据网格的大小,它将过滤掉绝大多数不需要的二维点。不幸的是,我正在运行的嵌入式系统内存严重受限,因此大型(按照我们的标准而不是其他任何人)2d 数组将过于占用内存。

我一直在尝试使用 KD 树来加快计算速度,但我无法找到与圆形扇区和 kd 树相关的算法。

是否有一种有效的算法可以找到圆形扇区内的二维点?

请注意,我们的特定系统在浮点数学和三角学方面都很慢,因此涉及较少的解决方案是需要大量解决方案的优秀解决方案。

【问题讨论】:

    标签: geometry intersection


    【解决方案1】:

    @Oren Trutner 的回答很棒,所以我决定制作一个视觉示例并进行一些改进以使其适用于所有角度。

    不多说,看下面的例子。

    $(document).on('keypress',function (e) {
            if(e.which === 13)
            {
                $("#calc").click();
            }
        });
    
        function areClockwise(v1, v2) {
            return -v1.x*v2.y + v1.y*v2.x > 0;
        }
    
        function vector(x = 0, y = 0) {
            return {x:x,y:y}
        }
    
        function degToRad(degree) {
            return degree * Math.PI / 180;
        }
    
        function isIn()
        {
            let illustration = $("#illustration");
            illustration.html("");
            let r = 250;
            let fieldOfViewAngle = 150;
            let x = Number($("#x").val());
            let y = Number($("#y").val());
            let startAngle = Number($("#startAngle").val());
            let startSectorAngle = degToRad(startAngle);
            let endSectorAngle = degToRad(startAngle+fieldOfViewAngle);
    
            $("#startLine").attr("x2",250 + r*Math.cos(-startSectorAngle)).attr("y2",250 + r*Math.sin(-startSectorAngle));
            $("#endLine").attr("x2",250 + r*Math.cos(-endSectorAngle)).attr("y2",250 + r*Math.sin(-endSectorAngle));
            $("#point").attr("cx",250 +x).attr("cy",250 -y);
    
            let sectorStartVector = vector(r * Math.cos(startSectorAngle),r * Math.sin(startSectorAngle));
            let sectorEndVector = vector(r * Math.cos(endSectorAngle),r * Math.sin(endSectorAngle));
            let relPoint = vector(x,y);
    
            if(!this.areClockwise(sectorStartVector, relPoint) &&
                this.areClockwise(sectorEndVector, relPoint))
                $("#result").html("Result: in");
            else{
                $("#result").html("Result: out")
            }
        }
    .flixy {
                display: flex;
                flex-direction: column;
            }
    
            .flixy > div {
                margin-bottom: 20px;
                width:300px
            }
    
            .flixy > div > input {
                float: right;
            }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <div id="result"></div>
    <div class="flixy">
        <div class="input-group">
            <label>X</label>
            <input id="x">
        </div>
        <div class="input-group">
            <label>Y</label>
            <input id="y">
        </div>
    
        <div class="input-group">
            <label>Start angle</label>
            <input id="startAngle">
        </div>
    
        <div class="input-group">
            <label>Radius</label>
            <input value="250" disabled>
        </div>
    
        <div class="input-group">
            <label>Theta</label>
            <input value="150" disabled>
        </div>
    </div>
    
    <button onclick="isIn()" id="calc">calc</button>
    
    <div style="width: 500px;height: 500px; overflow: visible">
        <svg width="500" height="500" style="overflow: visible">
            <circle cx="250" cy="250" r="250" stroke="black" stroke-width="3" fill="yellow"></circle>
            <line id="startLine" x1="250" y1="250" x2="500" y2="250" style="stroke:#2fa360;stroke-width:2" />
            <line id="endLine" x1="250" y1="250" x2="500" y2="250" style="stroke:#1d68a7;stroke-width:2" />
            <circle id="point" cx="250" cy="250" r="5" fill="red"></circle>
        </svg>
    </div>

    【讨论】:

      【解决方案2】:

      仅通过整数算术和加减乘基本运算就可以检查一个点是否在扇区内。

      对于一个位于circular sector 内的点,它必须满足以下测试:

      1. 必须从扇区的起始“臂”逆时针定位。
      2. 它必须从扇区的末端臂顺时针定位。
      3. 它必须比扇形半径更接近圆心。

      顺时针测试

      要测试一个向量 v2 是否顺时针指向另一个向量 v1,请执行以下操作:

      1. 找到v1的逆时针normal vector。法线向量与原始向量成 90 度角。这是straightforward to do:如果v1=(x1,y1),那么逆时针法线是n1=(-y1,x1)

      2. 求v2在法线上的投影大小。这可以通过计算 v2 的dot product 和法线来完成。

        projection = v2.x*n1.x + v2.y*n1.y

      3. 如果投影为正数,则 v2 将逆时针定位到 v1。否则,v2 顺时针到 v1。

      这是一个逆时针的例子:

      还有一个顺时针的例子:

      这些步骤可以合并:

      function areClockwise(v1, v2) {
        return -v1.x*v2.y + v1.y*v2.x > 0;
      }
      

      半径测试

      半径测试很简单。只需检查点到圆心的距离是否小于所需的半径。为了避免计算平方根,我们可以将距离的平方与半径的平方进行比较。

      function isWithinRadius(v, radiusSquared) {
        return v.x*v.x + v.y*v.y <= radiusSquared;
      }
      

      把它放在一起

      完整的扇区测试如下所示:

      function isInsideSector(point, center, sectorStart, sectorEnd, radiusSquared) {
        var relPoint = {
          x: point.x - center.x,
          y: point.y - center.y
        };
      
        return !areClockwise(sectorStart, relPoint) &&
               areClockwise(sectorEnd, relPoint) &&
               isWithinRadius(relPoint, radiusSquared);
      }
      

      以下示例页面通过数千个点演示了这一点。您可以在http://jsbin.com/oriyes/8/edit 上试用代码。

      示例源代码

      <!DOCTYPE html>
      <html>
        <head>
          <script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
          <style>
            .canvas {
              position: absolute;
              background: #f4f4f4;
              border: 8px solid #f4f4f4;
              width: 400px;
              height: 400px;
            }
      
            .dot {
              position: absolute;
              font: 16px Arial;
            }
            .out { color: #ddd; }
            .in { color: #00dd44; }
          </style>
          <script>
            function isInsideSector(point, center, sectorStart, sectorEnd, radiusSquared) {
              var relPoint = {
                x: point.x - center.x,
                y: point.y - center.y
              };
      
              return !areClockwise(sectorStart, relPoint) &&
                     areClockwise(sectorEnd, relPoint) &&
                     isWithinRadius(relPoint, radiusSquared);
            }
      
            function areClockwise(v1, v2) {
              return -v1.x*v2.y + v1.y*v2.x > 0;
            }
      
            function isWithinRadius(v, radiusSquared) {
              return v.x*v.x + v.y*v.y <= radiusSquared;
            }
      
            $(function() {
              var $canvas = $("#canvas");
              var canvasSize = 400;
              var count = 4000;
      
              // define the sector
              var center = { x: canvasSize / 2, y: canvasSize / 2 };
              var sectorStart = { x: 4, y: 1 };
              var sectorEnd = { x: 1, y: 4 };
              var radiusSquared = canvasSize * canvasSize / 4;
      
              // create, draw and test a number of random points
              for (var i = 0; i < count; ++i) {
      
                // generate a random point
                var point = {
                  x: Math.random() * canvasSize,
                  y: Math.random() * canvasSize
                };
      
                // test if the point is inside the sector
                var isInside = isInsideSector(point, center, sectorStart, sectorEnd, radiusSquared);
      
                // draw the point
                var $point = $("<div class='dot'></div>")
                    .css({
                      left: point.x - 3,
                      top:  canvasSize - point.y - 8 })
                    .html("&#8226;")
                    .addClass(isInside ? "in" : "out")
                    .appendTo($canvas);
              }
            });
          </script>
        </head>
        <body>
          <div id="canvas" class="canvas"></div>
        </body>
      </html>
      

      注意事项、注意事项和限制

      1. 您必须根据向量指定扇区的边界。例如,上面的屏幕截图显示了在 (4,1) 和 (1,4) 的向量之间拉伸的扇区。

      2. 如果您的部门是用其他术语指定的,例如角度,您必须先将其转换为矢量,例如使用tan() 函数。幸运的是,您只需执行一次。

      3. 这里的逻辑适用于内角小于 180 度的扇区。如果您的扇区可以跨越更大的角度,则必须对其进行修改。

      4. 此外,代码假定您知道扇区的哪个边界向量是“开始”,哪个是“结束”。如果你不知道,你可以在他们身上运行areClockwise() 来找出答案。

      5. 1234563确保使用足够位的整数来保存结果。

      【讨论】:

      • +1:我不知道这个答案是否正确(尽管我认为它是正确的)但它值得一票以奖励您投入的时间。
      • 谢谢,这看起来不错。我将不得不考虑如何最有效地将其应用到我的程序中,但它看起来很有希望。
      • 请记住,如果您有钝角,则需要进行额外检查。
      • 添加到我之前的评论中,我在使用 javascript 实现时必须解决的棘手问题是:a) 角度需要从 0 指向东方,逆时针方向,例如 90 代表正北。我认为这是在您的回答中暗示或说明的,但是这个数学笨蛋并没有完全理解 b) 将半径 (r) 和角度 (a) 转换为矢量坐标,使用 x = r * Math.cos(a) , y = r * Math.sin(a) 角度必须以弧度为单位(即 (度 * Math.PI) / 180)。我在学校已经很久没有这样做了,希望这可以帮助我这个职位的其他人!
      • 这个解决方案非常完美!非常感谢!这是您的解决方案的 Java 实现,如果有人需要,我已经完成(随意使用它):github.com/mvaguimaraes/…
      【解决方案3】:

      我知道您不需要三角函数,但您可以将每个点(在您的子集中)转换为其极坐标(原点是您的特定点)和阈值r,theta 其中r &lt; RT1 &lt; theta &lt; T2 对应到该部门。这当然是内存效率!

      【讨论】:

      • 我想过,但不幸的是,每次调用此操作时,具体点都会改变,而且调用很多!不过这是个好主意。
      猜你喜欢
      • 2012-07-10
      • 2021-01-25
      • 2015-07-29
      • 1970-01-01
      • 1970-01-01
      • 2016-08-17
      • 2016-08-03
      • 2013-08-23
      • 2011-10-08
      相关资源
      最近更新 更多