Fastest way to sort vectors by angle without actually computing that angle

后端 未结 7 1314
长情又很酷
长情又很酷 2020-12-09 04:19

Many algorithms (e.g. Graham scan) require points or vectors to be sorted by their angle (perhaps as seen from some other point, i.e. using difference vectors). This order i

相关标签:
7条回答
  • 2020-12-09 04:29

    I know one possible such function, which I will describe here.

    # Input:  dx, dy: coordinates of a (difference) vector.
    # Output: a number from the range [-1 .. 3] (or [0 .. 4] with the comment enabled)
    #         which is monotonic in the angle this vector makes against the x axis.
    def pseudoangle(dx, dy):
        ax = abs(dx)
        ay = abs(dy)
        p = dy/(ax+ay)
        if dx < 0: p = 2 - p
        # elif dy < 0: p = 4 + p
        return p
    

    So why does this work? One thing to note is that scaling all input lengths will not affect the ouput. So the length of the vector (dx, dy) is irrelevant, only its direction matters. Concentrating on the first quadrant, we may for the moment assume dx == 1. Then dy/(1+dy) grows monotonically from zero for dy == 0 to one for infinite dy (i.e. for dx == 0). Now the other quadrants have to be handled as well. If dy is negative, then so is the initial p. So for positive dx we already have a range -1 <= p <= 1 monotonic in the angle. For dx < 0 we change the sign and add two. That gives a range 1 <= p <= 3 for dx < 0, and a range of -1 <= p <= 3 on the whole. If negative numbers are for some reason undesirable, the elif comment line can be included, which will shift the 4th quadrant from -1…0 to 3…4.

    I don't know if the above function has an established name, and who might have published it first. I've gotten it quite a while ago and copied it from one project to the next. I have however found occurrences of this on the web, so I'd consider this snipped public enough for re-use.

    There is a way to obtain the range [0 … 4] (for real angles [0 … 2π]) without introducing a further case distinction:

    # Input:  dx, dy: coordinates of a (difference) vector.
    # Output: a number from the range [0 .. 4] which is monotonic
    #         in the angle this vector makes against the x axis.
    def pseudoangle(dx, dy):
        p = dx/(abs(dx)+abs(dy)) # -1 .. 1 increasing with x
        if dy < 0: return 3 + p  #  2 .. 4 increasing with x
        else:      return 1 - p  #  0 .. 2 decreasing with x
    
    0 讨论(0)
  • 2020-12-09 04:40

    I started to play around with this and realised that the spec is kind of incomplete. atan2 has a discontinuity, because as dx and dy are varied, there's a point where atan2 will jump between -pi and +pi. The graph below shows the two formulas suggested by @MvG, and in fact they both have the discontinuity in a different place compared to atan2. (NB: I added 3 to the first formula and 4 to the alternative so that the lines don't overlap on the graph). If I added atan2 to that graph then it would be the straight line y=x. So it seems to me that there could be various answers, depending on where one wants to put the discontinuity. If one really wants to replicate atan2, the answer (in this genre) would be

    # Input:  dx, dy: coordinates of a (difference) vector.
    # Output: a number from the range [-2 .. 2] which is monotonic
    #         in the angle this vector makes against the x axis.
    #         and with the same discontinuity as atan2
    def pseudoangle(dx, dy):
        p = dx/(abs(dx)+abs(dy)) # -1 .. 1 increasing with x
        if dy < 0: return p - 1  # -2 .. 0 increasing with x
        else:      return 1 - p  #  0 .. 2 decreasing with x
    

    This means that if the language that you're using has a sign function, you could avoid branching by returning sign(dy)(1-p), which has the effect of putting an answer of 0 at the discontinuity between returning -2 and +2. And the same trick would work with @MvG's original methodology, one could return sign(dx)(p-1).

    Update In a comment below, @MvG suggests a one-line C implementation of this, namely

    pseudoangle = copysign(1. - dx/(fabs(dx)+fabs(dy)),dy)
    

    @MvG says it works well, and it looks good to me :-).

    enter image description here

    0 讨论(0)
  • 2020-12-09 04:41

    The simpliest thing I came up with is making normalized copies of the points and splitting the circle around them in half along the x or y axis. Then use the opposite axis as a linear value between the beginning and end of the top or bottom buffer (one buffer will need to be in reverse linear order when putting it in.) Then you can read the first then second buffer linearly and it will be clockwise, or second and first in reverse for counter clockwise.

    That might not be a good explanation so I put some code up on GitHub that uses this method to sort points with an epsilion value to size the arrays.

    https://github.com/Phobos001/SpatialSort2D

    This might not be good for your use case because it's built for performance in graphics effects rendering, but it's fast and simple (O(N) Complexity). If your working with really small changes in points or very large (hundreds of thousands) data sets then this won't work because the memory usage might outweigh the performance benefits.

    0 讨论(0)
  • 2020-12-09 04:45

    If you can feed the original vectors instead of angles into a comparison function when sorting, you can make it work with:

    • Just a single branch.
    • Only floating point comparisons and multiplications.

    Avoiding addition and subtraction makes it numerically much more robust. A double can actually always exactly represent the product of two floats, but not necessarily their sum. This means for single precision input you can guarantee a perfect flawless result with little effort.

    This is basically Cimbali's solution repeated for both vectors, with branches eliminated and divisions multiplied away. It returns an integer, with sign matching the comparison result (positive, negative or zero):

    signed int compare(double x1, double y1, double x2, double y2) {
        unsigned int d1 = x1 > y1;
        unsigned int d2 = x2 > y2;
        unsigned int a1 = x1 > -y1;
        unsigned int a2 = x2 > -y2;
    
        // Quotients of both angles.
        unsigned int qa = d1 * 2 + a1;
        unsigned int qb = d2 * 2 + a2;
    
        if(qa != qb) return((0x6c >> qa * 2 & 6) - (0x6c >> qb * 2 & 6));
    
        d1 ^= a1;
    
        double p = x1 * y2;
        double q = x2 * y1;
    
        // Numerator of each remainder, multiplied by denominator of the other.
        double na = q * (1 - d1) - p * d1;
        double nb = p * (1 - d1) - q * d1;
    
        // Return signum(na - nb)
        return((na > nb) - (na < nb));
    }
    
    0 讨论(0)
  • 2020-12-09 04:45

    nice.. here is a varient that returns -Pi , Pi like many arctan2 functions.

    edit note: changed my pseudoscode to proper python.. arg order changed for compatibility with pythons math module atan2(). Edit2 bother more code to catch the case dx=0.

    def pseudoangle( dy , dx ):
      """ returns approximation to math.atan2(dy,dx)*2/pi"""
      if dx == 0 :
          s = cmp(dy,0)
      else::
          s = cmp(dx*dy,0)  # cmp == "sign" in many other languages.
      if s == 0 : return 0 # doesnt hurt performance much.but can omit if 0,0 never happens
      p = dy/(dx+s*dy)
      if dx < 0: return p-2*s
      return  p
    

    In this form the max error is only ~0.07 radian for all angles. (of course leave out the Pi/2 if you don't care about the magnitude.)

    Now for the bad news -- on my system using python math.atan2 is about 25% faster Obviously replacing a simple interpreted code doesnt beat a compiled intrisic.

    0 讨论(0)
  • 2020-12-09 04:49

    Just use a cross-product function. The direction you rotate one segment relative to the other will give either a positive or negative number. No trig functions and no division. Fast and simple. Just Google it.

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