通过角度对矢量进行排序的最快方法,而无需实际计算该角度

时间:2013-05-14 11:30:55

标签: performance sorting math geometry angle

许多算法(例如Graham scan)要求点或向量按其角度排序(可能从某些其他点看,即使用差异向量)。这个顺序本质上是循环的,并且在这个循环被破坏以计算线性值的情况下通常无关紧要。但是,只要维持循环次序,真实角度值也无关紧要。因此,对每个点进行atan2调用可能会浪费。有哪些更快的方法来计算角度严格单调的值,atan2的方式是什么?这些功能显然被一些人称为“伪角”。

8 个答案:

答案 0 :(得分:14)

我开始玩这个,并意识到规范有点不完整。 atan2有一个不连续性,因为当dx和dy变化时,atan2会在-pi和+ pi之间跳转。下图显示了@MvG建议的两个公式,实际上它们与atan2相比在不同的地方都存在不连续性。 (注意:我在第一个公式中添加了3,在替代方案中添加了4个,以便线条在图表上不重叠)。如果我将atan2添加到该图表,则它将是直线y = x。所以在我看来,可能有各种答案,取决于人们想要将不连续性放在哪里。如果一个人真的想要复制atan2,答案(在这个类型中)将是

# 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

这意味着如果您使用的语言具有符号功能,则可以通过返回符号(dy)(1-p)来避免分支,这样可以在返回之间的不连续处回答0 -2和+2。同样的技巧也适用于@ MvG的原始方法,可以返回符号(dx)(p-1)。

更新在下面的评论中,@ MVG建议采用单线C实现,即

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

@MvG说它运作良好,对我来说看起来不错: - )。

enter image description here

答案 1 :(得分:5)

我知道一种可能的功能,我将在这里描述。

# 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

那么为什么这样呢?需要注意的一点是,缩放所有输入长度不会影响输出。所以向量(dx, dy)的长度是无关紧要的,只有它的方向很重要。专注于第一象限,我们暂时可以假设dx == 1。然后dy/(1+dy)dy == 0的零单调增长到无限dy的一个(即dx == 0)。现在还必须处理其他象限。如果dy为否定,则原始p也是如此。因此,对于正dx,我们已经在角度中具有范围-1 <= p <= 1单调。对于dx < 0,我们更改了标志并添加了两个。这为1 <= p <= 3提供了范围dx < 0,并且整体上提供了范围-1 <= p <= 3。如果负数出于某种原因不受欢迎,则可以包含elif注释行,这会将第四象限从-1…0转移到3…4

我不知道上面的函数是否具有已建立的名称,以及谁可能首先发布它。我很久以前就已经把它从一个项目复制到另一个项目了。然而,我在网上发现了occurrences,所以我认为这个剪辑是公开的,可以重复使用。

有一种方法可以获得范围[0 ... 4](对于实际角度[0 ...2π])而不引入进一步的区分:

# 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

答案 2 :(得分:2)

我有点像三角学,所以我知道将角度映射到我们通常具有的某些值的最佳方法是切线。当然,如果我们想要一个有限的数字,以便没有比较{sign(x),y​​ / x}的麻烦,那就会让人感到更加困惑。

但是有一个函数映射[1,+ inf [到[1,0] [称为逆,这将允许我们有一个有限的范围,我们将映射角度。切线的倒数是众所周知的余切,因此x / y(是的,就像那样简单)。

一个小插图,显示单位圆上的切线和余切值:

values of tangent and cotangent

当| x |时,您会看到值相同= | y |,你还可以看到,如果我们在两个圆上为在[-1,1]之间输出值的部分着色,我们设法为整圆着色。要使这些值的映射连续且单调,我们可以做两件事:

  • 使用与余切相反的单调与切线相同
  • 将2添加到-cotan,使值与tan = 1
  • 重合
  • 将圆圈的四分之一(例如,在x = -y对角线下方)添加4,以使值适合其中一个不连续点。

这给出了以下分段函数,它是角度的连续且单调的函数,只有一个不连续性(这是最小的):

continuous piecewise function of the angles into (-1,8) piecewise definition of pseudo-angle

double pseudoangle(double dx, double dy) 
{
    // 1 for above, 0 for below the diagonal/anti-diagonal
    int diag = dx > dy; 
    int adiag = dx > -dy;

    double r = !adiag ? 4 : 0;

    if (dy == 0)
        return r;

    if (diag ^ adiag)
        r += 2 - dx / dy; 
    else
        r += dy / dx; 

    return r;
}

请注意,这非常接近Fowler angles,具有相同的属性。形式上,pseudoangle(dx,dy) + 1 % 8 == Fowler(dx,dy)

谈论性能,它比Fowler的代码(并且通常不那么复杂的imo)要小得多。在gcc 6.1.1上用-O3编译,上面的函数生成一个包含4个分支的汇编代码,其中两个来自dy == 0(一个检查两个操作数是否“无序”,因此如果dy是NaN,另一个是检查它们是否相等。

我认为这个版本比其他版本更精确,因为它只使用尾数保留操作,直到将结果移动到正确的间隔。当| x |时,这应该特别明显&LT;&LT; | Y |或| y | &GT;&GT; | x |,然后是操作| x | + | y |失去了一些精确度。

正如您在图表中看到的,角度 - 伪角度关系也非常接近线性。


查看分支的来源,我们可以发表以下意见:

  • 我的代码不依赖于abscopysign,这使它看起来更加独立。然而,在浮点值上使用符号位实际上是相当微不足道的,因为它只是翻转一个单独的位(没有分支!),所以这更不利。

  • 此处提出的其他解决方案在除以之前不会检查abs(dx) + abs(dy) == 0是否有效,但只要一个组件(dy)为0,此版本就会失败 - 因此抛出一个分支(或者在我的情况下2)。

如果我们选择获得大致相同的结果(直到舍入错误)但没有分支,我们可以滥用copsign并写:

double pseudoangle(double dx, double dy) 
{
    double s = dx + dy; 
    double d = dx - dy; 
    double r = 2 * (1.0 - copysign(1.0, s)); 
    double xor_sign = copysign(1.0, d) * copysign(1.0, s); 

    r += (1.0 - xor_sign);
    r += (s - xor_sign * d) / (d + xor_sign * s);

    return r;
}

如果dx和dy的绝对值接近,则由于d或s中的取消,可能会发生比之前实现更大的错误。没有检查除零除以与所呈现的其他实现相比,并且因为这仅在dx和dy都为0时发生。

答案 3 :(得分:1)

如果您可以在排序时将原始矢量而不是角度输入比较函数,则可以使其适用于:

  • 只是一个分支。
  • 仅浮点比较和乘法。

避免加法和减法使其在数值上更加稳健。实际上,double实际上可以完全代表两个浮点数的乘积,但不一定是它们的总和。这意味着对于单精度输入,您可以轻松保证完美无瑕的结果。

对于两个向量,这基本上是Cimbali's solution重复,分支被消除并且除法相乘。它返回一个整数,符号与比较结果匹配(正,负或零):

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

答案 4 :(得分:0)

很好..这里是一个返回-Pi的变量,Pi像许多arctan2函数一样。

编辑注释:将我的伪模式更改为正确的python ..更改了arg顺序,以便与pythons math模块atan2()兼容。编辑2打扰更多代码来捕获案例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

在这种形式下,所有角度的最大误差仅为~0.07弧度。 (当然,如果你不关心幅度,可以省略Pi / 2.)

现在有了坏消息 - 在我的系统上使用python math.atan2的速度提高了大约25% 显然,替换一个简单的解释代码并没有打败编译好的内部。

答案 5 :(得分:0)

我想出的最简单的事情就是制作点的标准化副本,并沿着x或y轴将圆圈分成两半。然后使用相反的轴作为顶部或底部缓冲区的开始和结束之间的线性值(当放入时,一个缓冲区将需要以反向线性顺序。)然后,您可以线性地读取第一个缓冲区和第二个缓冲区,它将逆时针方向,顺时针方向,第二方向和第一方向相反方向。

这可能不是一个很好的解释,所以我在GitHub上放了一些代码,使用这个方法对带有epsilion值的点进行排序以调整数组的大小。

https://github.com/Phobos001/SpatialSort2D

这可能不适合您的用例,因为它是为了提高图形效果渲染性能而构建的,但它快速而简单(O(N)复杂性)。如果您使用点数或非常大(数十万)数据集进行非常小的更改,那么这将无法正常工作,因为内存使用量可能会超过性能优势。

答案 6 :(得分:0)

如果角度本身不需要,而只是为了排序,那么@jjrv 方法是最好的方法。这是 Julia 中的比较

using StableRNGs
using BenchmarkTools

# Definitions
struct V{T}
  x::T
  y::T
end

function pseudoangle(v)
    copysign(1. - v.x/(abs(v.x)+abs(v.y)), v.y)
end

function isangleless(v1, v2)
    a1 = abs(v1.x) + abs(v1.y)
    a2 = abs(v2.x) + abs(v2.y)

    a2*copysign(a1 - v1.x, v1.y) < a1*copysign(a2 - v2.x, v2.y)
end

# Data
rng = StableRNG(2021)
vectors = map(x -> V(x...), zip(rand(rng, 1000), rand(rng, 1000)))

# Comparison
res1 = sort(vectors, by = x -> pseudoangle(x));
res2 = sort(vectors, lt = (x, y) -> isangleless(x, y));

@assert res1 == res2

@btime sort($vectors, by = x -> pseudoangle(x));
  # 110.437 μs (3 allocations: 23.70 KiB)

@btime sort($vectors, lt = (x, y) -> isangleless(x, y));
  # 65.703 μs (3 allocations: 23.70 KiB)

因此,通过避免除法,时间几乎减半而不会降低结果质量。当然,为了更精确的计算,isangleless 应该不时配备 bigfloat,但同样可以告诉 pseudoangle

答案 7 :(得分:-3)

只需使用跨产品功能。您将一个段相对于另一个段旋转的方向将给出正数或负数。没有三角函数也没有除法。快速而简单。只是Google吧。