避免使用atan2计算角度 - 改进atan2的精度

3

作为开发人员,我们经常需要计算角度以执行旋转。通常我们可以使用 atan2() 函数,但有时需要更高的精度。那么你该怎么办呢?

我知道在理论上 atan2 是精确的,但在我的系统(iOS)中,它的精度约为 0.05 弧度,差异很大。这不仅是我的问题。我看到了类似的意见。


1
你需要比1E-15弧度更高的精度吗?真的吗? - Ignacio Vazquez-Abrams
不,这不是重复的问题。这是关于iOS和其他系统中atan2问题的一般性问题。在这里,我并不是为了解决上述问题而提问(因为我已经用不同的方法解决了它),我只是想了解一下。我认为其他开发人员有时会遇到类似的困难,所以这是一个普遍性的问题。 - wczekalski
1
如果您有一个可重现的 atan2 产生错误答案的示例,无论在哪个平台上,请向平台供应商提交错误报告。在这种情况下,始终使用 bugreport.apple.com。 - Stephen Canon
2
如果你要声称atan2的误差为+/-0.05是荒谬的,那么你应该给出一些产生如此大误差的输入值的具体示例。 - rob mayoff
4个回答

3
atan2用于从向量(x,y)中获取角度a。如果您使用此角度应用旋转,则将使用cos(a)sin(a)。您可以通过对(x,y)进行归一化并保留它们而不是角度来简单地计算cos和sin。精度将更高,并且您将节省在三角函数中失去的大量周期。

编辑。如果您确实需要从(x,y)获得角度,则可以使用CORDIC的变体计算所需的精度。


2
经常使用角度吗?不,你不会。在我看到开发人员使用角度的10次中,有7次应该使用线性代数而不是避免任何三角函数。
旋转最好使用矩阵而不是角度。也可以参考这个问题: CGAffineTranformRotate atan2 inaccuration

我希望我能给这个答案一万个赞!在我看来,当你使用atan2计算角度(即知道delta_xdelta_y)时,三角函数的滥用比例接近95%... - gboffi

2
在iOS上,我发现标准三角函数运算符精确到大约13或14个小数位,所以你看到0.05弧度的误差听起来非常奇怪。如果您可以提供能够证明这一点的代码和特定值,请在此行为上提交错误报告(并在此处发布代码,以便我们记录下来)。
话虽如此,如果您真的需要高精度的三角函数运算符,我修改了Dave DeLong为他的DDMathParser代码创建的一些例程。这些例程使用NSDecimal进行数学计算,可提供长达约34个十进制数字的小数精度,同时避免了用于表示10进制小数的标准浮点问题。您可以从此处下载这些修改后的例程的代码。
以下是atan()的NSDecimal版本计算代码:
NSDecimal DDDecimalAtan(NSDecimal x) {
    // from: http://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Infinite_series

    // The normal infinite series diverges if x > 1
    NSDecimal one = DDDecimalOne();
    NSDecimal absX = DDDecimalAbsoluteValue(x);

    NSDecimal z = x;
    if (NSDecimalCompare(&one, &absX) == NSOrderedAscending) 
    {
        // y = x / (1 + sqrt(1+x^2))
        // Atan(x) = 2*Atan(y)
        // From: http://www.mathkb.com/Uwe/Forum.aspx/math/14680/faster-Taylor-s-series-of-Atan-x

        NSDecimal interiorOfRoot;
        NSDecimalMultiply(&interiorOfRoot, &x, &x, NSRoundBankers);
        NSDecimalAdd(&interiorOfRoot, &one, &interiorOfRoot, NSRoundBankers);
        NSDecimal denominator = DDDecimalSqrt(interiorOfRoot);
        NSDecimalAdd(&denominator, &one, &denominator, NSRoundBankers);
        NSDecimal y;
        NSDecimalDivide(&y, &x, &denominator, NSRoundBankers);

        NSDecimalMultiply(&interiorOfRoot, &y, &y, NSRoundBankers);
        NSDecimalAdd(&interiorOfRoot, &one, &interiorOfRoot, NSRoundBankers);
        denominator = DDDecimalSqrt(interiorOfRoot);
        NSDecimalAdd(&denominator, &one, &denominator, NSRoundBankers);
        NSDecimal y2;
        NSDecimalDivide(&y2, &y, &denominator, NSRoundBankers);

//        NSDecimal two = DDDecimalTwo();
        NSDecimal four = DDDecimalFromInteger(4);
        NSDecimal firstArctangent = DDDecimalAtan(y2);

        NSDecimalMultiply(&z, &four, &firstArctangent, NSRoundBankers);
    }
    else
    {
        BOOL shouldSubtract = YES;
        for (NSInteger n = 3; n < 150; n += 2) {
            NSDecimal numerator;
            if (NSDecimalPower(&numerator, &x, n, NSRoundBankers) == NSCalculationUnderflow)
            {
                numerator = DDDecimalZero();
                n = 150;
            }

            NSDecimal denominator = DDDecimalFromInteger(n);

            NSDecimal term;
            if (NSDecimalDivide(&term, &numerator, &denominator, NSRoundBankers) == NSCalculationUnderflow)
            {
                term = DDDecimalZero();
                n = 150;
            }

            if (shouldSubtract) {
                NSDecimalSubtract(&z, &z, &term, NSRoundBankers);
            } else {
                NSDecimalAdd(&z, &z, &term, NSRoundBankers);
            }

            shouldSubtract = !shouldSubtract;
        }
    }

    return z;
}

这里使用了泰勒级数逼近法,并加入了一些快捷方式以加速收敛。我认为在接近 Pi / 4 弧度的结果中,精度可能不会达到完整的34位数字,所以我可能还需要修复它。

如果您需要极高的精度,这是一个选择,但是如果您使用的是 double 值,那么您报告的情况不应该发生,因此这里有一些奇怪的地方。


2

如果在您的系统中,long doubledouble 更精确,您可以使用 atan2l

long double atan2l(long double y, long double x);

4
在当前的ARM iOS设备上,“long double”仅被映射到“double”,因此使用它不会获得任何精度。您将在iOS模拟器中获得额外的精度,因为它在Mac上运行。 - Brad Larson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接