如何为sf线要素交点内部转角做圆角处理实现道路平滑渲染
我正在使用OSM数据制作矢量街道地图,通过为OSM提供的道路线要素添加缓冲区生成道路面。本问题仅和几何处理相关,与OSM无关,因此用基础线要素做简化演示:
library(dplyr) library(ggplot2) library(sf) # 两条代表交叉道路的线 l1 <- st_as_sfc(c("LINESTRING(0 0,0 5)","LINESTRING(-5 3,5 3)")) # 合并两条线(这样缓冲后二者是相交关系而非重叠) l2 <- l1 %>% st_union() ggplot()+ geom_sf(data = st_buffer(l2, dist = 0.75))
运行代码得到的交叉口效果如下:
我最终希望实现谷歌、OSM、必应等地图API的道路渲染效果,为交叉口内部转角做圆角处理,预期效果如下:
我查阅了sf包的st_系列方法,仅找到st_buffer可控制端帽形状、外角角度数量的参数,未找到处理内角的相关方案。请问是否有简单实用的实现方法?
更新
Lovalery和mrhellmann提供了两种解决方案,我最终选择了mrhellmann的方案适配我的需求,同时我对比了两种方案的优劣势如下,供不同使用场景的用户参考:
先定义用到的不同处理方法:
l1 <- st_as_sfc(c("LINESTRING(0 0,0 5)","LINESTRING(-5 3,5 3)")) l2 <- l1 %>% st_union() l2.base <- l2 %>% st_buffer(dist = 0.75, endCapStyle = "ROUND") # 原始方案 l2.neg <- l2.base %>% st_buffer(dist = -0.25) # mrhellmann方案(负缓冲区法) l2.smth <- l2.base %>% smooth(method = "ksmooth", smoothness = 3, n= 50L)# Lovalery方案(smoothr平滑法)
Lovalery指出负缓冲区法也会对线段端点做平滑处理。对比三种方法可见,负缓冲区法除了影响端帽外,还会按设置的缓冲区距离向所有方向收缩几何要素。
l2.neg的问题可以通过将原缓冲区距离加上平滑所用的负缓冲区数值(0.75 + 0.25)解决:
l3 <- l1 %>% st_union %>% st_buffer(dist = 0.75+0.25, endCapStyle = "ROUND") l3.neg <- st_buffer(l3, dist = -0.25)

经过调整后两种方法效果接近,l3.neg的结果比l2.smth更贴近原l2几何形态,更推荐l3.neg法的原因如下:
- 仅使用sf包无需额外依赖
- 几何还原度更高
st_buffer的距离参数使用与地图CRS一致的单位,比smoothr的平滑度参数(基于顶点平均距离的系数)更易理解
但Lovalery提出的线段端点精度问题也值得注意:st_buffer默认的ROUND端帽样式会按缓冲区距离延长线段长度。如果绘图范围会裁剪所有道路端点则无影响,若需要渲染道路端点则需保证长度准确。
更合适的缓冲区设置是endCapStyle = FLAT,此时l2.smth的smoothness参数需要从3调整为0.2。
l2和l2.smth方法的线段长度渲染符合预期,l2.neg和l3.neg保留了端点的90度角(非必需的优化点),但线段长度会因负缓冲区距离缩短,这一点smoothr更有优势。
最后要注意smoothr生成的曲线不对称,平滑度数值越高、顶点长度差异越大,不对称越明显,下图是smoothness=1时的效果:
可见较长顶点处的曲线有拉长现象,多数渲染尺度下该问题不明显。
正如Lovalery所说,方案选择取决于具体使用场景,普通用户很难注意到道路比OSM标注短1米或是曲线轻微不对称,图形渲染的精度需求可按需平衡。
完整代码如下:
library(dplyr) library(ggplot2) library(sf) library(smoothr) l1 <- st_as_sfc(c("LINESTRING(0 0,0 5)","LINESTRING(-5 3,5 3)")) l2 <- l1 %>% st_union() #### endCapStyle = ROUND 场景 #### l2.base <- l2 %>% st_buffer(dist = 0.75, endCapStyle = "ROUND") l2.neg <- l2 %>% st_buffer(dist = -0.25) l2.smth <- l2 %>% smooth(method = "ksmooth", smoothness = 3, n= 50L) l3 <- l1 %>% st_union %>% st_buffer(dist = 0.75+0.25, endCapStyle = "ROUND") l3.neg <- st_buffer(l3, dist = -0.25) ggplot()+ geom_sf(data = l2.base, colour = "darkgreen", fill = "palegreen")+ geom_sf(data = l2.neg, colour = "blue", fill = NA)+ geom_sf(data = l2.smth, colour = "red", fill = NA)+ geom_sf(data = l3.neg, colour = "cyan", fill = NA)+ ggtitle("方法对比", subtitle = "ROUND端帽:原始方案(绿色) vs 平滑法(红色) vs 初始负缓冲区(蓝色) vs 调整后负缓冲区(青色)") + scale_y_continuous(breaks = c(-1:6), limits = c(-1,6))+ scale_x_continuous(breaks = c(-6:6), limits = c(-6,6)) #### endCapStyle = FLAT 场景 #### l2.base <- l1 %>% st_union() %>% st_buffer(dist = 0.75, endCapStyle = "FLAT") l2.neg <- l2.base %>% st_buffer(dist = -0.25) l2.smth <- l2.base %>% smooth(method = "ksmooth", smoothness = 0.2, n= 50L) l3 <- l1 %>% st_union %>% st_buffer(dist = 0.75+0.25, endCapStyle = "FLAT") l3.neg <- st_buffer(l3, dist = -0.25) ggplot()+ geom_sf(data = l2.base, colour = "darkgreen", fill = "palegreen")+ geom_sf(data = l2.neg, colour = "blue", fill = NA)+ geom_sf(data = l2.smth, colour = "red", fill = NA)+ geom_sf(data = l3.neg, colour = "cyan", fill = NA)+ ggtitle("方法对比", subtitle = "FLAT端帽:原始方案(绿色) vs 平滑法(红色) vs 初始负缓冲区(蓝色) vs 调整后负缓冲区(青色)") + scale_y_continuous(breaks = c(-1:6), limits = c(-1,6))+ scale_x_continuous(breaks = c(-6:6), limits = c(-6,6)) #### 展示smoothr方法的不对称性 #### l4.smth <- l2.base %>% smooth(method = "ksmooth", smoothness = 1, n= 50L) ggplot()+ geom_sf(data = l2.base, colour = "darkgreen", fill = "palegreen")+ geom_sf(data = l4.smth, colour = "red", fill = NA)+ geom_sf(data = l3.neg, colour = "blue", fill = NA)+ geom_abline(slope = 1, intercept = 3, colour = "gray", linetype = "dashed")+ ggtitle("方法对比", subtitle = "FLAT端帽:调整后负缓冲区(蓝色) vs smoothr平滑度=1(红色)") + coord_sf(xlim = c(0,5), ylim = c(3,5))
内容的提问来源于stack exchange,提问作者AWaddington

