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

1、抽稀

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

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

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

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

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

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

OK,終于到代碼登場(chǎng)了,不廢話,上代碼:
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) {
        // 找到最大閾值點(diǎn),即操作(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;
            }
        }

        // 如果存在最大閾值點(diǎn),就進(jìn)行遞歸遍歷出所有最大閾值點(diǎn)
        List<Point> result = new ArrayList<>();
        if (maxH > epsilon) {
            List<Point> leftPoints = new ArrayList<>();// 左曲線
            List<Point> rightPoints = new ArrayList<>();// 右曲線
            // 分別提取出左曲線和右曲線的坐標(biāo)點(diǎn)
            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ù)點(diǎn)
            leftResult.addAll(rightResult);
            result = leftResult;
        } else {// 如果不存在最大閾值點(diǎn)則返回當(dāng)前遍歷的子曲線的起始點(diǎn)
            result.add(points.get(0));
            result.add(points.get(end - 1));
        }
        return result;
    }

    /**
     * 計(jì)算點(diǎn)到直線的距離
     * 
     * @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;
    }

    /**
     * 計(jì)算兩點(diǎn)之間的距離
     * 
     * @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)基本實(shí)現(xiàn)了!但是如果換成經(jīng)緯度呢?其實(shí)不用擔(dān)心,地圖API一般都會(huì)提供計(jì)算兩個(gè)經(jīng)緯度坐標(biāo)之間距離的函數(shù),所以萬(wàn)變不離其宗,思路還是一樣的,大膽點(diǎn),代碼啪啪啪的敲起來吧!

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容