Efficiently find points inside a circle sector

前端 未结 3 1151
伪装坚强ぢ
伪装坚强ぢ 2020-11-30 23:02

I have a set of 2d points distributed randomly. I need to perform a time intensive operation on a small subset of these points but I need to first figure out what points I n

相关标签:
3条回答
  • 2020-11-30 23:44

    It's possible to check if a point is inside a sector with only integer arithmetic and the basic operations of addition, subtraction and multiplication.

    For a point to be inside a circular sector, it has to meet the following tests:

    1. It has to be positioned counter-clockwise from the start "arm" of the sector.
    2. It has to be positioned clockwise from the end arm of the sector.
    3. It has to be closer to the center of the circle than the sector's radius.

      For a point to be inside a, it has to meet the following tests It has to be positioned counter-clockwise from the start "arm" of the sector It has to be positioned clockwise from the end arm of the sector It has to be closer to the center of the circle than the sector's radius

    Clockwise tests

    To test if a vector v2 is clockwise to a another vector v1, do the following:

    1. Find the counter-clockwise normal vector of v1. The normal vector is at a 90 degrees angle to the original vector. This is straightforward to do: if v1=(x1,y1), then the counter-clockwise normal is n1=(-y1,x1).

    2. Find the size of the projection of v2 on the normal. This can be done by calculating the dot product of v2 and the normal.

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

    3. If the projection is a positive number, then the v2 is positioned counter-clockwise to v1. Otherwise, v2 is clockwise to v1.

    Here's a counter-clockwise example: Counter-clockwise example

    And a clockwise example: Clockwise example

    The steps can be combined:

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

    Radius test

    The radius test is straightforward. Just check if the distance of the point from the center of the circle is less than the desired radius. To avoid computing square roots, we can compare the square of the distance with the square of the radius instead.

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

    Putting it together

    The complete sector test looks something like:

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

    The following sample page demonstrates this over several thousand points. You can experiment with the code at: http://jsbin.com/oriyes/8/edit.

    Screenshot

    Sample source code

    <!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: #ffffd; }
          .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>
    

    Notes, caveats and limitations

    1. You have to specify the boundaries of the sector in terms of vectors. The screenshot above, for example, shows a sector stretching between the vectors of (4,1) and (1,4).

    2. If your sector is specified in other terms, e.g. angles, you will have to convert that to vectors first, e.g. using the tan() function. Fortunately, you only have to do this once.

    3. The logic here works for sectors with an inner angle of less than 180 degrees. If your sectors can span a larger angle, you'll have to modify it.

    4. Additionally, the code assumes that you know which of the bounding vectors of the sector is the "start" and which is the "end". If you don't, you can run the areClockwise() on them to find out.

    5. Note that while all this can be done with integer arithmetic, both the radius and clockwise tests use a larger range of numbers, due to squaring x's and y's and multiplying them together. Make sure to use integers of sufficient bits to hold the results.

    0 讨论(0)
  • 2020-11-30 23:51

    I know you don't want trigonometry, but you could convert each point (in your subset) to its polar coordinates (where the origin is your specific point) and threshold r,theta where r < R and T1 < theta < T2 corresponding to the sector. It's certainly memory efficient!

    0 讨论(0)
  • 2020-11-30 23:52

    @Oren Trutner answer was great so I decided to make a visual example of it and make some improvements to make it work on all angles.

    no further speaking, check the example below.

    $(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>

    0 讨论(0)
提交回复
热议问题