如何在浮点支持较差的处理器上计算 GPS 坐标之间的距离?

发布于 2024-11-29 20:48:15 字数 549 浏览 1 评论 0原文

我需要计算 GPS 坐标之间的距离来计算行驶距离。我已经尝试过Haversine和Vincenty算法,它们在我的台式PC上运行良好,但是当我将代码移植到dsPIC时,由于缺乏浮点精度,它们对于接近的点(几米之内)返回0以及 sin 和 cos 的糟糕实现。

对于我的用例,我的点相距不超过 10 米,并且彼此之间的距离都在 10 公里以内。我尝试过以下算法,结果看起来不错:

double dist(double latA, double lonA, double latB, double lonB)
{
    double latD = fabs(latA - latB) * 111.3;
    double lonD = fabs(lonA - lonB) * 111.3 * cos(latA * 3.14159265 / 180);

    return sqrt(latD*latD + lonD*lonD) * 1000;
}

假设每1°的距离是111.3公里,我使用毕达哥拉斯定理来计算距离。有什么简单的方法可以改进我的算法吗?或者还有其他不依赖于高精度正弦/余弦的算法吗?

I need to calculate the distance between GPS co-ordinates to calculate distance traveled. I've tried both the Haversine and Vincenty algorithms, which work fine on my desktop PC, but when I port the code to the dsPIC, they return 0 for points that are close (within several meters) due to a lack of floating point precision and poor implementations of sin and cos.

For my use case, my points will be no more than 10 meters apart and will all fall within 10km of each other. I've tried the following algorithm and the results seem ok:

double dist(double latA, double lonA, double latB, double lonB)
{
    double latD = fabs(latA - latB) * 111.3;
    double lonD = fabs(lonA - lonB) * 111.3 * cos(latA * 3.14159265 / 180);

    return sqrt(latD*latD + lonD*lonD) * 1000;
}

Assuming the distance for every 1° is 111.3km, I use the pythagorean theorem to calculate distance. Is there any easy way to improve my algorithm? Or are there any other algorithms that don't depend on a highly accurate sin/cos?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

秋叶绚丽 2024-12-06 20:48:16

海洋 AIS 系统(在 IEC61193-2 中指定)中使用的公认算法是 Rhumb Line算法。我已经使用 Anthony 在目标上成功实现了这一点Williams 的定点数学库,它使用 CORDIC算法,我相信通常会比软件浮点性能提高约 5 倍。

然而,该库是 C++ 而不是 C,由于大量的运算符重载,这使得它易于使用,但这可能不是您想要的。然而,值得考虑对 C 代码使用 C++ 编译,只是为了这个库的好处。当然,问题是 Microchip 的 C31 编译器奇怪地不支持 C++。

但需要注意的是,log() 函数的查找表只差一个值,因此需要在末尾添加一个值为 0 的附加元素。我找到后安东尼证实了这一点,但我不相信他更新了下载。

不管怎样,答案可能是使用定点和 CORDIC。

要解析 1m 的经度或赤道弧,您将需要 8 位精度,因此单精度浮点数是不够的,使用双精度会大大减慢速度。检查 MikroElectronica 的 C 用户手册发现编译器仅支持单精度 - floatdoublelong double 都是 32 位,所以有在任何情况下,使用此编译器都无法使用内置 FP 类型来实现所需的准确性。

如果有任何用处,这是我使用安东尼库的 Rhumb Line 代码:

标题:

#if !defined cRhumbLine_INCLUDE
#define cRhumbLine_INCLUDE

#include "fixed.hpp"

//! Rhumb Line method for distance and bearing between two geodesic points
class cRhumbLine
{
public:

    //! @brief Default constructor
    //!
    //! Defines a zero length line, bearing zero
    cRhumbLine() : m_update_bearing(false), m_update_distance(false), m_distance(0), m_bearing(0) {} 

    //! @brief Constructor defining a line
    //!
    //! @param startLatDeg  Start latitude in degrees, negative values are south of equator
    //! @param startLonDeg  Start longitude in degrees, negative values are west of meridian.
    //! @param endLatDeg    End latitude in degrees, negative values are south of equator
    //! @param endLonDeg    End longitude in degrees, negative values are west of meridian.
    cRhumbLine( fixed startLatDeg, fixed startLonDeg, fixed endLatDeg, fixed endLonDeg ) 
    {
        define( startLatDeg, startLonDeg, endLatDeg, endLonDeg ) ;
    }

    //! @brief Define a start/ent point
    //!
    //! @param startLatDeg  Start latitude in degrees, negarive values are south of equator
    //! @param startLonDeg  Start longitude in degrees, negative values are west of meridian.
    //! @param endLatDeg    End latitude in degrees, negarive values are south of equator
    //! @param endLonDeg    End longitude in degrees, negative values are west of meridian.
    void define( fixed startLatDeg, fixed startLonDeg, fixed endLatDeg, fixed endLonDeg ) ;

    //! @brief Get start-end distance in nautical miles
    //! @return Start-end distance in nautical miles.
    fixed distanceNm() { return distanceMetres() / ONE_NM_IN_METRE ; }

    //! @brief Get start-end distance in metres.
    //! @return Start-end distance in metres.
    fixed distanceMetres()  ;

    //! @brief Get start-end bearing in degreed.
    //! @return Start-end bearing in degreed (0 <= x < 360).
    fixed bearingDeg()  ;

private:
    static const int ONE_NM_IN_METRE = 1852 ;

    bool m_update_bearing ;
    bool m_update_distance ;
    fixed m_distance ;
    fixed m_bearing ;

    fixed m_delta_phi ;
    fixed m_delta_lon ;
    fixed m_delta_lat ;
    fixed m_lat1_radians ;
} ;

#endif

正文:

#include "cRhumbLine.h"

void cRhumbLine::define( fixed startLatDeg, fixed startLonDeg, fixed endLatDeg, fixed endLonDeg )
{
    fixed lat1 = startLatDeg / 180 * fixed_pi ;
    fixed lon1 = startLonDeg / 180 * fixed_pi ;
    fixed lat2 = endLatDeg / 180 * fixed_pi ;
    fixed lon2 = endLonDeg / 180 * fixed_pi ;

    m_update_bearing = true ;
    m_update_distance = true ;

    m_delta_phi = log( tan( lat2 / 2 + fixed_quarter_pi ) / tan( lat1 / 2 + fixed_quarter_pi ) ) ;
    m_delta_lat = lat1 - lat2 ;
    m_delta_lon = lon1 - lon2 ;
    m_lat1_radians = lat1 ;

    // Deal with delta_lon > 180deg, take shorter route across meridian
    if( m_delta_lon.abs() > fixed_pi )
    {
        m_delta_lon = m_delta_lon > 0 ? -(fixed_two_pi - m_delta_lon) : (fixed_two_pi + m_delta_lon) ;
    }
} 


fixed cRhumbLine::distanceMetres()
{
    if( m_update_distance )
    {
        static const fixed mean_radius = 6371009 ; // in metres. Source: International Union of Geodesy and Geophysics (IUGG)

        fixed q = m_delta_phi != 0 ? m_delta_lat / m_delta_phi : cos( m_lat1_radians ) ; // Deal with lines with constant latitude

        m_distance = sqrt( ( sqr(m_delta_lat) + sqr(q) * sqr(m_delta_lon) ) ) * mean_radius ;
        m_update_distance = false ;
    }

    return m_distance ;
}


fixed cRhumbLine::bearingDeg()
{
    if( m_update_bearing )
    {
        static const fixed mean_radius = 6371009 ; // in metres. Source: International Union of Geodesy and Geophysics (IUGG)

        m_bearing = atan2( m_delta_lon, m_delta_phi ) / fixed_pi * 180 ;
        if( m_bearing == -180 )
        {
            m_bearing == 0 ;
        }
        else if( m_bearing < 0 )
        {
            m_bearing += 360 ;
        }
        m_update_bearing = false ;
    }

    return m_bearing ;
}

The accepted algorithm for use in Marine AIS systems (specified in IEC61193-2) is the Rhumb Line algorithm. I have successfully implemented this on a target using Anthony Williams' fixed point maths library, which uses the CORDIC algorithm, and will I believe typically give a bout 5x performance improvement over software floating point.

However the library is C++ rather than C, which makes it easy to use due to extensive operator overloading, but is not perhaps what you are looking for. Worth considering using C++ compilation for your C code however, just for the benefit of this library. The problem with that of course is that Microchip's C31 compiler bizarrely does not support C++.

One caveat however is that the look-up table for the log() function is too short by one value and needs an additional element at the end with value zero. This was confirmed by Anthony after I found it, but I do not believe that he has updated the download.

Either way, the answer is probably to use fixed point, and CORDIC.

To resolve to 1m of longitude or equatorial arc, you will need 8 digits of precision, so a single precision float will be insufficient, using double precision will slow things considerably. Checking MikroElectronica's C User Manual reveals that the compiler only supports single precision - float, double, and long double are all 32-bit, so there is no way to achieve the accuracy you need using the built-in FP types in any case with this compiler.

If it is of any use, here is my Rhumb Line code using Anthony's library:

Header:

#if !defined cRhumbLine_INCLUDE
#define cRhumbLine_INCLUDE

#include "fixed.hpp"

//! Rhumb Line method for distance and bearing between two geodesic points
class cRhumbLine
{
public:

    //! @brief Default constructor
    //!
    //! Defines a zero length line, bearing zero
    cRhumbLine() : m_update_bearing(false), m_update_distance(false), m_distance(0), m_bearing(0) {} 

    //! @brief Constructor defining a line
    //!
    //! @param startLatDeg  Start latitude in degrees, negative values are south of equator
    //! @param startLonDeg  Start longitude in degrees, negative values are west of meridian.
    //! @param endLatDeg    End latitude in degrees, negative values are south of equator
    //! @param endLonDeg    End longitude in degrees, negative values are west of meridian.
    cRhumbLine( fixed startLatDeg, fixed startLonDeg, fixed endLatDeg, fixed endLonDeg ) 
    {
        define( startLatDeg, startLonDeg, endLatDeg, endLonDeg ) ;
    }

    //! @brief Define a start/ent point
    //!
    //! @param startLatDeg  Start latitude in degrees, negarive values are south of equator
    //! @param startLonDeg  Start longitude in degrees, negative values are west of meridian.
    //! @param endLatDeg    End latitude in degrees, negarive values are south of equator
    //! @param endLonDeg    End longitude in degrees, negative values are west of meridian.
    void define( fixed startLatDeg, fixed startLonDeg, fixed endLatDeg, fixed endLonDeg ) ;

    //! @brief Get start-end distance in nautical miles
    //! @return Start-end distance in nautical miles.
    fixed distanceNm() { return distanceMetres() / ONE_NM_IN_METRE ; }

    //! @brief Get start-end distance in metres.
    //! @return Start-end distance in metres.
    fixed distanceMetres()  ;

    //! @brief Get start-end bearing in degreed.
    //! @return Start-end bearing in degreed (0 <= x < 360).
    fixed bearingDeg()  ;

private:
    static const int ONE_NM_IN_METRE = 1852 ;

    bool m_update_bearing ;
    bool m_update_distance ;
    fixed m_distance ;
    fixed m_bearing ;

    fixed m_delta_phi ;
    fixed m_delta_lon ;
    fixed m_delta_lat ;
    fixed m_lat1_radians ;
} ;

#endif

Body:

#include "cRhumbLine.h"

void cRhumbLine::define( fixed startLatDeg, fixed startLonDeg, fixed endLatDeg, fixed endLonDeg )
{
    fixed lat1 = startLatDeg / 180 * fixed_pi ;
    fixed lon1 = startLonDeg / 180 * fixed_pi ;
    fixed lat2 = endLatDeg / 180 * fixed_pi ;
    fixed lon2 = endLonDeg / 180 * fixed_pi ;

    m_update_bearing = true ;
    m_update_distance = true ;

    m_delta_phi = log( tan( lat2 / 2 + fixed_quarter_pi ) / tan( lat1 / 2 + fixed_quarter_pi ) ) ;
    m_delta_lat = lat1 - lat2 ;
    m_delta_lon = lon1 - lon2 ;
    m_lat1_radians = lat1 ;

    // Deal with delta_lon > 180deg, take shorter route across meridian
    if( m_delta_lon.abs() > fixed_pi )
    {
        m_delta_lon = m_delta_lon > 0 ? -(fixed_two_pi - m_delta_lon) : (fixed_two_pi + m_delta_lon) ;
    }
} 


fixed cRhumbLine::distanceMetres()
{
    if( m_update_distance )
    {
        static const fixed mean_radius = 6371009 ; // in metres. Source: International Union of Geodesy and Geophysics (IUGG)

        fixed q = m_delta_phi != 0 ? m_delta_lat / m_delta_phi : cos( m_lat1_radians ) ; // Deal with lines with constant latitude

        m_distance = sqrt( ( sqr(m_delta_lat) + sqr(q) * sqr(m_delta_lon) ) ) * mean_radius ;
        m_update_distance = false ;
    }

    return m_distance ;
}


fixed cRhumbLine::bearingDeg()
{
    if( m_update_bearing )
    {
        static const fixed mean_radius = 6371009 ; // in metres. Source: International Union of Geodesy and Geophysics (IUGG)

        m_bearing = atan2( m_delta_lon, m_delta_phi ) / fixed_pi * 180 ;
        if( m_bearing == -180 )
        {
            m_bearing == 0 ;
        }
        else if( m_bearing < 0 )
        {
            m_bearing += 360 ;
        }
        m_update_bearing = false ;
    }

    return m_bearing ;
}
逆蝶 2024-12-06 20:48:16

一些评论:

  • 您需要指定计算的范围和精度要求。在确定使用什么方法计算余弦时,范围和精度极其重要。此外,如果纬度和经度的相对差异与到极点的角距离相比很小,您发布的毕达哥拉斯近似效果很好。如果纬度不接近,您的伪毕达哥拉斯算法在高纬度地区将无法正常工作。 (例如,在纬度 43.001 和 43.002 处效果很好,但在纬度 89.961 和 89.962 处则不然)

  • 需要根据其圆度来计算经度 - 您的算法将在国际日期变更线周围失败,但这可以通过采用纵向差分对称模 360 来轻松解决,其中 smod(x,m) = mod(x+m/2,m)-m/2。 (例如 -179.5 - +179.5 = -359 度,但如果计算 smod(-359,360),您将得到 +1 度。)

  • 在设计时,充分利用您的PC。您有一个非常强大的计算器,您可以评估大量测试点的高精度答案和您的近似值,并查看它们有何不同,以评估准确性。如果您在此信息中推断出模式,则可以使用它进行二阶近似以提高准确性。


更新:您声明您的范围/精度要求为 +/-60 度(在一个半球内缩小范围没有优势)和 1% 的精度。 cos(x) 的良好近似值(x 为度数)在此范围内为 c2(x) = 0.9942 - 1.39*10-4 * x 2 = 0.9942 - (0.01179x)2;在此范围内其误差最大值为0.006。

如果您想要更高的精度,请使用四次多项式 (c4(x) = 0.999945-(0.01233015x)2+(0.007778x)4 在此范围内的最大误差小于 6x10-5,但对参数错误和算术更加敏感错误)或分成多个范围。

Some comments:

  • You need to specify the range and the accuracy requirements of your computation. Range and accuracy are extremely important in determining what approach you use to calculate cosines. In addition, the pythagorean approximation you posted works well if the relative differences in latitude and longitude are small compared to the angular distance to the pole. Your pseudo-pythagorean algorithm won't work well at high latitudes if the latitudes aren't close together. (e.g. with latitude 43.001 and 43.002 it would work well, but not at 89.961 and 89.962)

  • Longitudes need to be calculated in view of their circularness -- Your algorithm will fail around the international date line, but that can be easily remedied by taking the longitudinal difference symmetric-modulo 360, where smod(x,m) = mod(x+m/2,m)-m/2. (e.g. -179.5 - +179.5 = -359 degrees, but if you compute smod(-359,360) you get +1 degrees.)

  • At design time, make good use of your PC. You have a very powerful calculator available, and you could evaluate for a large # of test points the high-precision answer and your approximation, and see how they differ, to evaluate accuracy. If you deduce a pattern in this information, you could use it to make a 2nd-order approximation to increase accuracy.


update: You state that your range/accuracy requirements are +/-60 degrees (no advantage in reducing range in one hemisphere) and 1% accuracy. A good approximation of cos(x), with x in degrees, within this range is c2(x) = 0.9942 - 1.39*10-4 * x2 = 0.9942 - (0.01179x)2; its error over this range has a maximum value of 0.006.

If you want better accuracy use a 4th degree polynomial (c4(x) = 0.999945-(0.01233015x)2+(0.007778x)4 has a maximum error over this range of less than 6x10-5, but is much more sensitive to parameter errors and arithmetic errors) or split into multiple ranges.

负佳期 2024-12-06 20:48:16

您可能想尝试使用预先计算的 sin 和 cos 表。
它使用更多的内存,可以破坏通用处理器(不是您的情况)上的缓存,但在您的处理器上具有尽可能高的准确性,并且速度相当快。

You may want to try to use a precomputed table for sin and cos.
It uses more memory, can trash the cache on general purpose processor (not your case) but will have as much accuracy as possible on your processor and will be quite fast.

凹づ凸ル 2024-12-06 20:48:16

您(有效地)使用定点 DSP,因此您可能想研究定点函数;他们可能会表现得更好。

事实证明,Microchip 有一个可用的定点库: http://www.microchip.com/stellent/idcplg?IdcService=SS_GET_PAGE&nodeId=2680&dDocName=en552208 我不知道这会有多大帮助 - 它可能缺乏您需要的精度。

以下是如何自行操作的示例: http://www.coranac.com/2009 /07/sines

回到正轨 - Microchip 页面表明他们的编译器和库与单精度和双精度兼容 IEEE-754。除非他们说的是半真半假并且使用半精度(16 位)格式。如果您仍然没有得到您需要的结果,我会考虑提交错误报告。

You're on a fixed point DSP (effectively) so you might want to look into fixed-point functions; they are likely to perform better.

Turns out that Microchip have a fixed-point library available: http://www.microchip.com/stellent/idcplg?IdcService=SS_GET_PAGE&nodeId=2680&dDocName=en552208 I don't know how helpful that will be - it may lack the precision you need.

And here is an example of how to do it yourself: http://www.coranac.com/2009/07/sines

Back on track - the Microchip page suggests that their compiler and library are IEEE-754 compatible for both single and double precision. Unless they're telling a half-truth and are using the half-precision (16-bit) format. If you still aren't getting the results you need I'd consider filing a bug report.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文