用于凸包敏感性分析的空间多边形重叠百分比

Percentage overlap of spatial polygons for a sensitivity analysis of convex hull

为了可重复性,让我们将我的问题简化如下:我有 100 个空间多边形,代表从总体(100 次)中抽取的 N 个随机样本的凸包,以计算模型对单个值的敏感性。 如何计算这些多边形的重叠百分比?理想的解决方案应该是快速的并且引入尽可能少的近似值。

我没有特别的理由使用 R 的 GIS 功能,除了我认为这可能是解决问题的最简单方法。

library(sp)
library(raster)
library(sf)
#> Linking to GEOS 3.8.1, GDAL 3.1.1, PROJ 6.3.1

set.seed(11)

dt <- data.frame(x = rnorm(1e3, 10, 3) + sample(-5:5, 1e3, replace = TRUE))
dt$y <- (rnorm(1e3, 3, 4) + sample(-10:10, 1e3, replace = TRUE)) + dt$x

dt <- rbind(dt, data.frame(x = -dt$x, y = dt$y))

plot(dt, asp = 1)

dt.chull <- dt[chull(dt),]
dt.chull <- rbind(dt.chull, dt.chull[1,])

lines(dt.chull, col = "green")

uncert.polys <- lapply(1:100, function(i) {

tmp <- dt[sample(rownames(dt), 1e2),]

# points(tmp, col = "red")

tmp <- tmp[chull(tmp),]
tmp <- rbind(tmp, tmp[1,])

tmp <- sp::SpatialPolygons(list(sp::Polygons(list(sp::Polygon(tmp)), ID = i)))

sp::SpatialPolygonsDataFrame(tmp, data = data.frame(id = i, row.names = i))

# lines(tmp, col = "red")

})

polys <- do.call(rbind, uncert.polys)

plot(polys, add = TRUE, border = "red")

我最初的尝试是使用 sf::st_intersection 函数:

sf.polys <- sf::st_make_valid(sf::st_as_sf(polys))
all(sf::st_is_valid(sf.polys))
#> [1] TRUE

sf::st_intersection(sf.polys)
#> Error in CPL_nary_intersection(x): Evaluation error: TopologyException: found non-noded intersection between LINESTRING (-9.80706 -0.619557, -7.66331 -3.55177) and LINESTRING (-9.80706 -0.619557, -9.80706 -0.619557) at -9.8070645468969637 -0.61955676978603658.

该错误可能与多边形线有关 "that are almost coincident but not identical". Multiple solutions (1, 2) 已被建议解决此 GEOS 相关问题,none 我已设法使用我的数据:

sf.polys <- sf::st_set_precision(sf.polys, 1e6) 

sf.polys <- sf::st_snap(sf.polys, sf.polys, tolerance = 1e-4)

sf::st_intersection(sf.polys)
#> Error in CPL_nary_intersection(x): Evaluation error: TopologyException: found non-noded intersection between LINESTRING (-13.7114 32.7341, 3.29417 30.3736) and LINESTRING (3.29417 30.3736, 3.29417 30.3736) at 3.2941702528617176 30.373627946201278.

所以,我必须使用光栅化来近似多边形重叠:

GT <- sp::GridTopology(cellcentre.offset = c(round(min(dt$x),1), round(min(dt$y),1)), 
                       cellsize = c(diff(round(range(dt$x), 1))/100, diff(round(range(dt$y), 1))/100),
                       cells.dim = c(100, 100)
)

SG <- sp::SpatialGrid(GT)

tmp <- lapply(seq_along(uncert.polys), function(i) {
  
  out <- sp::over(SG, uncert.polys[[i]])
  out[!is.na(out)] <- 1
  out[is.na(out)] <- 0
  out
})

tmp <- data.frame(overlapping.n = Reduce("+", lapply(tmp, "[[", 1)))
tmp$overlapping.pr <- 100*tmp$overlapping.n/100

uncert.data <- SpatialGridDataFrame(SG, tmp)

## Plot


plot(x = range(dt$x),
     y = range(dt$y), 
     type = "n"
)

plot(raster::raster(uncert.data), col = colorRampPalette(c("white", "red", "blue", "white"))(100), add = TRUE)
plot(polys, add = TRUE, border = adjustcolor("black", alpha.f = 0.2), cex = 0.5)
points(dt, pch = ".", col = "black", cex = 3)
lines(dt.chull, col = "green")

该方法给出了结果,但输出是近似值并且需要很长时间来处理。必须有更好的方法来做到这一点。

出于性能比较的目的,这是我当前的解决方案:

gridOverlap <- function(dt, uncert.polys) {
  GT <- sp::GridTopology(cellcentre.offset = c(round(min(dt$x),1), round(min(dt$y),1)), 
                         cellsize = c(diff(round(range(dt$x), 1))/100, diff(round(range(dt$y), 1))/100),
                         cells.dim = c(100, 100)
  )
  
  SG <- sp::SpatialGrid(GT)
  
  tmp <- lapply(seq_along(uncert.polys), function(i) {
    
    out <- sp::over(SG, uncert.polys[[i]])
    out[!is.na(out)] <- 1
    out[is.na(out)] <- 0
    out
  })
  
  tmp <- data.frame(overlapping.n = Reduce("+", lapply(tmp, "[[", 1)))
  tmp$overlapping.pr <- 100*tmp$overlapping.n/100
  
  SpatialGridDataFrame(SG, tmp)
}

system.time(gridOverlap(dt = dt, uncert.polys = uncert.polys))
#   user  system elapsed 
#   3.011   0.083   3.105 

性能对于较大的数据集很重要(此解决方案在实际应用中需要几分钟)。

reprex package (v0.3.0)

于 2020-09-01 创建

这是使用 spatstat 找到内部而没有任何错误的解决方案 和底层 polyclip 包。

library(spatstat)

# Data from OP
set.seed(11)
dt <- data.frame(x = rnorm(1e3, 10, 3) + sample(-5:5, 1e3, replace = TRUE))
dt$y <- (rnorm(1e3, 3, 4) + sample(-10:10, 1e3, replace = TRUE)) + dt$x
dt <- rbind(dt, data.frame(x = -dt$x, y = dt$y))

# Converted to spatstat classes (`ppp` not strictly necessary -- just a habit)
X <- as.ppp(dt, W = owin(c(-25,25),c(-15,40)))
p1 <- owin(poly = dt[rev(chull(dt)),])

# Plot of data and convex hull
plot(X, main = "")
plot(p1, add = TRUE, border = "green")

# Convex hulls of sampled points in spatstat format
polys <- lapply(1:100, function(i) {
  tmp <- dt[sample(rownames(dt), 1e2),]
  owin(poly = tmp[rev(chull(tmp)),])
})

# Plot of convex hulls
for(i in seq_along(polys)){
  plot(polys[[i]], add = TRUE, border = "red")
}

# Intersection of all convex hulls plotted in transparent blue
interior <- do.call(intersect.owin, polys)
plot(interior, add = TRUE, col = rgb(0,0,1,0.1))

我不清楚你想从这里做什么,但至少这种方法 避免了多边形裁剪的错误。

要在 spatstat 中执行基于网格的解决方案,我会将 windows 转换为 二进制图像蒙版,然后从那里开始工作:

Wmask <- as.im(Window(X), dimyx = c(200, 200))
masks <- lapply(polys, as.im.owin, xy = Wmask, na.replace = 0)
maskmean <- Reduce("+", masks)/100
plot(maskmean)

速度取决于你选择的分辨率,但我想应该是很多 比当前使用 sp/raster 的建议更快(可能 使用与此处相同的逻辑可以改进很多,所以那将是另一个 选择坚持 raster).

编辑在下方进一步修改了可能更好的解决方案。

考虑了一下这个问题,我倾向于三角剖分和动态规划方法。

  1. 考虑每个凸包的点和线。将它们标记为它们所属的外壳(可能存储在查找中)
  2. 取所有直线上的点并对它们进行三角剖分,这些三角形将被记录为它们在多少个凸包内。
  3. 此时有很多方法可以确定三角形有多少个凸包。您展示的示例倾向于一些可能的优化,但作为一般解决方案,最好的方法可能只是循环每个三角形,看看它在哪个船体中,O(T*H)
  4. 应该可以注意到 points/edges/triangles 并计算出每个包在哪些壳内(特别是每个边缘的左侧和右侧在哪个壳内,然后可以用来确定每个边缘内有哪些壳三角形(设置线内侧的船体的并集),并从中计算三角形所在的船体数量。棘手的一点是如何在不采用 O(T*H) 的情况下级联信息。想多了再回复。

用更好的方法编辑

Should their intersection be added to the list of points to be triangulated? Reducing the ambiguity. That technique is a linescan algorithm especially that for detecting intersections in O(Nlog(N)) time, such as the https://en.wikipedia.org/wiki/Bentley%E2%80%93Ottmann_algorithm

所以这里有一个更直接的更新方法包括下面的示例图像(看起来比预期的要小...)

上图显示了 3 个凸包,并为从左到右穿过每个点的扫掠线编号。尽管 Andrew's Algorithm for convex hulls 确实避免了对实际扫描线的需要,因为它是算法的一部分。基本上,您使用安德鲁的算法一次性构建所有船体,但有重复。

所以基本过程如下所示:

  1. 为每个已知船体(G/R/B:绿色、红色、黑色)、上下船体设置空列表。所以每个点到它们所在的船体的映射(初始化为空列表)。
  2. 使用安德鲁算法的排序顺序对所有点(在凸包内)进行排序。
  3. 使用与 Andrew 算法相同的排序顺序,将每个点添加到每个船体(上和下)。
  4. 我们再用安德鲁的算法来考虑点数。不过,诀窍在于我们已经知道船体将是什么。考虑红色船体,点 2,7 和 8。以及其他点 4 和 5(5 实际上是 2 点,忘记了标签)。 4 将作为船体点添加,但由于我们关注的是红色船体,因此我们忽略 4(因为它不在灰色船体内部)。如果多个船体使用相同的点,则同样适用,因为从技术上讲,该点不在任何这些船体内部(除非您想这样考虑,在这种情况下,所有船体点都在至少 1 个船体内,这可能很有用为了视觉上的好处,我认为这是使交叉点着色实用的唯一方法)。但是,这两个 5 点位于灰色外壳内,因此我们注意到它们都位于红色外壳内。整体性能大致为 O(N*C),其中 N 是点数,C 是外壳数。我想这可能会下降到 O(C log N + N log C) 之类的东西或付出足够的努力,但可能不值得。

您可以运行设置交叉点以找到所有交叉点,然后使用它们构建多边形以获得更精确的着色。然而,这让事情变得更加混乱,我仍在努力寻找一个好的解决方案。但是,我怀疑,将一个点算作“在它自己的船体中”可能会对此有很大帮助。在这种情况下,您可能只取构成多边形的点的最小值。因此,如果您在 1/2/2/2 个船体内有点,则该区域在 1 个船体内。

我会首先在多个船体中没有点的情况下进行测试。然后调整逻辑支持多壳。

为了获得最佳性能,我只会 运行 这个算法在实际船体点上,然后将结果(color-coded 多边形,如果你走线段路线)叠加在实际之上数据集,如果你需要的话。如果你没有采用颜色编码的多边形路线,那么我可能会根据它们所在的平均船体数量或者可能 运行 使用所有点(不仅仅是船体点)的算法来为多边形着色,但就是这样成为巨大的性能打击。只为线段做工作可能更好。