Efficiently find points inside a circle sector

25,027

Solution 1

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: #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>

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.

Solution 2

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!

Share:
25,027
ApockofFork
Author by

ApockofFork

Updated on May 28, 2020

Comments

  • ApockofFork
    ApockofFork almost 4 years

    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 need to perform this time intensive operation on. To determine what points I need they must pass a series of geometric criteria.

    The most basic criteria is are they within a certain distance of a specific point. The second most basic criteria is whether they are contained within a circle sector (a 2-D cone) extending out from that specific point. (Edit: This operation is called regularly with a different specific point each time but the same set of 2d points.)

    My initial thought was to create a grid containing the 2d points, then iterate along the cone grabbing grid squares that it intersects. Depending on the size of the grid it would filter out the vast majority of unneeded 2d points. Unfortunately the embedded system I'm running on is severely memory constrained so a large (by our standards not anyone elses) 2d array would be too memory intensive.

    I have been trying to investigate using KD trees to speed up the calculation but I haven't been able to find an algorithm relating circle sectors and kd-trees.

    Is there an efficient algorithm for finding what 2d points lie within a circle sector?

    Just a note our particular system is slow at both floating point math and trigonometry so a solution that involves less of those is superior one that requires a lot of it.

  • ApockofFork
    ApockofFork over 11 years
    I thought about that but unfortunately the specific point changes every time this operation is called and its called a lot! Its a good idea though.
  • High Performance Mark
    High Performance Mark over 11 years
    +1: I don't know whether this answer is correct or not (though I think it is) but it deserves an upvote to reward the time you have devoted to it.
  • ApockofFork
    ApockofFork over 11 years
    Thanks this looks good. I'm going to have to think about how to most effectively apply this to my program but it looks promising.
  • Viesturs
    Viesturs over 10 years
    Keep in mind that if you have obtuse angles, you need to do additional checks.
  • Phani Rahul
    Phani Rahul over 10 years
    why couldn't i think of this? :D
  • Harshay Buradkar
    Harshay Buradkar over 9 years
    First of all I would like to thank the author for the good work in the answer. BUT there is a bug in it for cases where the central angle of the sector is more than 180 degrees. The reason for it being the clockwise tests fail if the angle between two vectors is more than 180 degrees. Here is the modified jsbin with the bug showing. jsbin.com/doducu/1/edit Note in the code that I modified the sectorStart and sectorEnd variable.
  • Bobby Jack
    Bobby Jack about 8 years
    This is an excellent answer, which I've already upvoted. However, can you explain point 2. with an example, please? For instance, say I have a point at 100, 100 'looking' North, with a sector from -30 degrees to +30 degrees and a radius of 50, how do I calculate the values for sectorStart and sectorEnd?
  • Bobby Jack
    Bobby Jack about 8 years
    To add to my previous comment, the tricky bits I had to resolve when working with a javascript implementation were: a) the angles need to go from 0 pointing east, counter-clockwise, such that 90 represents due north. I think this is implied or stated in your answer, but this mathematical dunce didn't quite get that b) to convert a radius (r) and angle (a) into vector coordinates, use x = r * Math.cos(a), y = r * Math.sin(a) the angle must be in radians (i.e. (degrees * Math.PI) / 180). It's been a long time since I did this at school, and hopefully this helps someone else in my position!
  • Francesco Boffa
    Francesco Boffa over 7 years
    Basically, this is the solution to the first problem of the the 2017 Facebook Hacker Cup.
  • koalo
    koalo over 7 years
    You can go around caveats 3 and 4 (which are basically the same) by inverting the logic in case they are not clockwise. Also fixes the bug by @HarshayBuradkar. See jsbin.com/vahuwodiwu/edit
  • Marcos Guimaraes
    Marcos Guimaraes over 6 years
    This solution is perfection! Thank you very much! This is a Java implementation of your solution that I have done in case anybody needs (feel free to use it): github.com/mvaguimaraes/…