路径简化中的Visvalingam & Whyatt算法与Douglas-Peucker算法

557 阅读2分钟

Visvalingam & Whyatt和Douglas-Peucker是两种具有代表性的路径简化算法,在场景如GPS路径记录、地图绘制时,能够有效减少绘制的数据点数量,提升绘制效率,降低运行成本。

名词解释

  • GIS(地理信息系统) 是一种用于捕获、存储、管理、分析和展示地理空间数据的系统。它通过地理位置将数据与地图结合,用于解决空间问题和支持决策。GIS 技术广泛应用于城市规划、交通管理、环境保护和灾害应对等领域。
  • 概化(Generalization) 是一种简化地图内容的过程,通过去掉次要细节并突出主要特征,使地图在不同的缩放比例下易于理解。

Visvalingam & Whyatt 算法

image.png

原理

Visvalingam & Whyatt 算法通过计算路径中三点居点所成三角形的有效面积(Effective Area),从小到大删除最小面积的点,以进行简化。

代码实现(Kotlin)

// 类代表路径中的点的坐标
data class Point(val x: Double, val y: Double)

// 已知顶点坐标计算三角形面积
fun calculateEffectiveArea(p1: Point, p2: Point, p3: Point): Double {
    return Math.abs((p1.x * (p2.y - p3.y) + p2.x * (p3.y - p1.y) + p3.x * (p1.y - p2.y)) / 2.0)
}

// 实现 Visvalingam & Whyatt 算法
fun simplifyPathVW(points: List<Point>, threshold: Double): List<Point> {
    if (points.size <= 2) return points

    val areas = points.mapIndexed { index, point ->
        if (index == 0 || index == points.size - 1) Double.MAX_VALUE
        else calculateEffectiveArea(points[index - 1], point, points[index + 1])
    }.toMutableList()

    val mutablePoints = points.toMutableList()

    while (areas.minOrNull()!! < threshold) {
        val minIndex = areas.indexOf(areas.minOrNull()!!)
        areas.removeAt(minIndex)
        mutablePoints.removeAt(minIndex)

        if (minIndex > 0 && minIndex < mutablePoints.size - 1) {
            areas[minIndex - 1] = calculateEffectiveArea(
                mutablePoints[minIndex - 1], mutablePoints[minIndex], mutablePoints[minIndex + 1]
            )
        }
    }

    return mutablePoints
}

Douglas-Peucker算法

image.png

原理

Douglas-Peucker 算法通过设定一个距离阈值,递归地找到离起始点和终止点构成的直线距离最远的点,如果该距离大于阈值,则保留该点并继续细分。否则,删除该点。

代码实现(Kotlin)

// 计算点到直线的垂直距离
fun perpendicularDistance(point: Point, start: Point, end: Point): Double {
    val numerator = Math.abs((end.y - start.y) * point.x - (end.x - start.x) * point.y + end.x * start.y - end.y * start.x)
    val denominator = Math.sqrt(Math.pow((end.y - start.y), 2.0) + Math.pow((end.x - start.x), 2.0))
    return numerator / denominator
}

// 实现 Douglas-Peucker 算法
fun simplifyPathDP(points: List<Point>, epsilon: Double): List<Point> {
    if (points.size <= 2) return points

    var maxDistance = 0.0
    var index = 0

    for (i in 1 until points.size - 1) {
        val distance = perpendicularDistance(points[i], points.first(), points.last())
        if (distance > maxDistance) {
            maxDistance = distance
            index = i
        }
    }

    return if (maxDistance > epsilon) {
        val firstSegment = simplifyPathDP(points.subList(0, index + 1), epsilon)
        val secondSegment = simplifyPathDP(points.subList(index, points.size), epsilon)
        firstSegment + secondSegment.drop(1)
    } else {
        listOf(points.first(), points.last())
    }
}

参考资料