Android GPS定位軌跡抽稀之道格拉斯-普克(Douglas-Peuker)算法詳解

1、抽稀

通俗點講,直接舉個栗子吧:我們知道運動軌跡實際上是由很多個經(jīng)緯度坐標(biāo)連接而成。那么我們是否需要將所有運動時記錄下來的經(jīng)緯度坐標(biāo)都用來繪制軌跡呢?其實是沒必要的,很多數(shù)據(jù)其實是多余的,實際上將這些多余的數(shù)據(jù)剔除仍然能保證軌跡曲線形狀大致不變,而且還能讓曲線更平滑更節(jié)省存儲空間,類似這樣的過程我們就稱之為抽稀。抽稀的算法很多,這里將介紹一種經(jīng)典的算法:道格拉斯-普克(Douglas-Peuker)算法。

2、道格拉斯-普克(Douglas-Peuker)算法

還是舉個栗子吧,假設(shè)在平面坐標(biāo)系上有一條由N個坐標(biāo)點組成的曲線,已設(shè)定一個閾值epsilon
(1)首先,將起始點與結(jié)束點用直線連接, 再找出到該直線的距離最大,同時又大于閾值epsilon的點并記錄下該點的位置(這里暫且稱其為最大閾值點),如圖所示:

(2)接著,以該點為分界點,將整條曲線分割成兩段(這里暫且稱之為左曲線和右曲線),將這兩段曲線想象成獨立的曲線然后重復(fù)操作(1),找出兩邊的最大閾值點,如圖所示:

(3)最后,重復(fù)操作(2)(1)直至再也找不到最大閾值點為止,然后將所有最大閾值點按順序連接起來便可以得到一條更簡化的,更平滑的,與原曲線十分近似的曲線,如圖所示:

2、如何實現(xiàn)?

OK,終于到代碼登場了,不廢話,上代碼:
Point類:

public class Point {
    double x;
    double y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
        System.out.print("(" + x + "," + y + ") ");
    }

    public static Point instance(int x, int y) {
        return new Point(x, y);
    }
}

DouglasPeuckerUtil 類:

public class DouglasPeuckerUtil {

    public static void main(String[] args) {

        System.out.print("原始坐標(biāo):");

        List<Point> points = new ArrayList<>();
        List<Point> result = new ArrayList<>();

        points.add(Point.instance(1, 1));
        points.add(Point.instance(2, 2));
        points.add(Point.instance(3, 4));
        points.add(Point.instance(4, 1));
        points.add(Point.instance(5, 0));
        points.add(Point.instance(6, 3));
        points.add(Point.instance(7, 5));
        points.add(Point.instance(8, 2));
        points.add(Point.instance(9, 1));
        points.add(Point.instance(10, 6));

        System.out.println("");
        System.out
                .println("=====================================================================");
        System.out.print("抽稀坐標(biāo):");

        result = DouglasPeucker(points, 1);

        for (Point p : result) {
            System.out.print("(" + p.x + "," + p.y + ") ");
        }
    }

    public static List<Point> DouglasPeucker(List<Point> points, int epsilon) {
        // 找到最大閾值點,即操作(1)
        double maxH = 0;
        int index = 0;
        int end = points.size();
        for (int i = 1; i < end - 1; i++) {
            double h = H(points.get(i), points.get(0), points.get(end - 1));
            if (h > maxH) {
                maxH = h;
                index = i;
            }
        }

        // 如果存在最大閾值點,就進(jìn)行遞歸遍歷出所有最大閾值點
        List<Point> result = new ArrayList<>();
        if (maxH > epsilon) {
            List<Point> leftPoints = new ArrayList<>();// 左曲線
            List<Point> rightPoints = new ArrayList<>();// 右曲線
            // 分別提取出左曲線和右曲線的坐標(biāo)點
            for (int i = 0; i < end; i++) {
                if (i <= index) {
                    leftPoints.add(points.get(i));
                    if (i == index)
                        rightPoints.add(points.get(i));
                } else {
                    rightPoints.add(points.get(i));
                }
            }

            // 分別保存兩邊遍歷的結(jié)果
            List<Point> leftResult = new ArrayList<>();
            List<Point> rightResult = new ArrayList<>();
            leftResult = DouglasPeucker(leftPoints, epsilon);
            rightResult = DouglasPeucker(rightPoints, epsilon);

            // 將兩邊的結(jié)果整合
            rightResult.remove(0);//移除重復(fù)點
            leftResult.addAll(rightResult);
            result = leftResult;
        } else {// 如果不存在最大閾值點則返回當(dāng)前遍歷的子曲線的起始點
            result.add(points.get(0));
            result.add(points.get(end - 1));
        }
        return result;
    }

    /**
     * 計算點到直線的距離
     * 
     * @param p
     * @param s
     * @param e
     * @return
     */
    public static double H(Point p, Point s, Point e) {
        double AB = distance(s, e);
        double CB = distance(p, s);
        double CA = distance(p, e);

        double S = helen(CB, CA, AB);
        double H = 2 * S / AB;

        return H;
    }

    /**
     * 計算兩點之間的距離
     * 
     * @param p1
     * @param p2
     * @return
     */
    public static double distance(Point p1, Point p2) {
        double x1 = p1.x;
        double y1 = p1.y;

        double x2 = p2.x;
        double y2 = p2.y;

        double xy = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
        return xy;
    }

    /**
     * 海倫公式,已知三邊求三角形面積
     * 
     * @param cB
     * @param cA
     * @param aB
     * @return 面積
     */
    public static double helen(double CB, double CA, double AB) {
        double p = (CB + CA + AB) / 2;
        double S = Math.sqrt(p * (p - CB) * (p - CA) * (p - AB));
        return S;
    }

輸出結(jié)果:

OK,平面坐標(biāo)上的Douglas-Peuker算法已經(jīng)基本實現(xiàn)了!但是如果換成經(jīng)緯度呢?其實不用擔(dān)心,地圖API一般都會提供計算兩個經(jīng)緯度坐標(biāo)之間距離的函數(shù),所以萬變不離其宗,思路還是一樣的,大膽點,代碼啪啪啪的敲起來吧!

這里有一個基于百度地圖實現(xiàn)的Demo供大家參考:
https://github.com/wnn1302/TrackDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容