znlgis 博客

GIS开发与技术分享

第14章:缓冲区与简化

缓冲区分析和几何简化是 GIS 空间分析中最常用的两类操作。缓冲区用于生成要素周围的影响范围区域,而简化用于降低几何复杂度以提高渲染和计算性能。本章将全面介绍 GeoPandas 中缓冲区与简化的相关方法。


14.1 缓冲区分析概述 - 空间分析中的重要性

14.1.1 什么是缓冲区

缓冲区(Buffer)是指围绕几何对象一定距离内的所有点组成的区域。缓冲区是空间分析中最基础、最常用的操作之一。

常见应用场景:

应用场景 说明
服务区分析 医院、学校等设施的服务覆盖范围
影响范围分析 污染源、噪声源的影响区域
安全距离 道路、铁路两侧的安全隔离带
邻近分析 查找距离某要素一定范围内的其他要素
数据融合 通过缓冲区合并相近的要素

14.1.2 缓冲区的数学定义

对于几何对象 G 和距离 d,缓冲区定义为:

Buffer(G, d) = { p | distance(p, G) ≤ d }

即所有与 G 的距离不超过 d 的点的集合。


14.2 buffer() 详解

14.2.1 基本语法

GeoSeries.buffer(distance, resolution=16, cap_style='round',
                  join_style='round', mitre_limit=5.0, single_sided=False)

14.2.2 参数说明

参数 类型 默认值 说明
distance float 必需 缓冲距离,正值向外扩展,负值向内收缩
resolution int 16 圆弧近似的线段数(每个四分之一圆弧)
cap_style str ‘round’ 线端头样式:’round’、’flat’、’square’
join_style str ‘round’ 连接处样式:’round’、’mitre’、’bevel’
mitre_limit float 5.0 mitre 连接的最大延伸比例
single_sided bool False 是否生成单侧缓冲区

14.2.3 基本示例

import geopandas as gpd
from shapely.geometry import Point, LineString, Polygon

# 点缓冲区 → 圆
point = Point(0, 0)
gs = gpd.GeoSeries([point])
buffer_point = gs.buffer(1.0)
print(f"点缓冲区面积: {buffer_point.iloc[0].area:.4f}")  # ≈ π

# 线缓冲区 → 带状区域
line = LineString([(0, 0), (5, 0)])
gs_line = gpd.GeoSeries([line])
buffer_line = gs_line.buffer(1.0)
print(f"线缓冲区面积: {buffer_line.iloc[0].area:.4f}")  # ≈ 5*2 + π

# 多边形缓冲区 → 扩大的多边形
poly = Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])
gs_poly = gpd.GeoSeries([poly])
buffer_poly = gs_poly.buffer(0.5)
print(f"多边形缓冲区面积: {buffer_poly.iloc[0].area:.4f}")

14.2.4 resolution 参数

resolution 控制圆弧的平滑度:

import geopandas as gpd
from shapely.geometry import Point

point = Point(0, 0)
gs = gpd.GeoSeries([point])

for res in [1, 4, 8, 16, 32, 64]:
    buf = gs.buffer(1.0, resolution=res)
    vertices = len(buf.iloc[0].exterior.coords)
    area = buf.iloc[0].area
    print(f"resolution={res:2d}: 顶点数={vertices:3d}, 面积={area:.6f} (π≈3.141593)")

14.3 正缓冲区与负缓冲区 - 膨胀与收缩

14.3.1 正缓冲区(膨胀)

正缓冲距离使几何对象向外扩展:

import geopandas as gpd
from shapely.geometry import Polygon

poly = Polygon([(0, 0), (4, 0), (4, 3), (0, 3)])
gs = gpd.GeoSeries([poly])

# 不同距离的正缓冲区
for d in [0.5, 1.0, 2.0]:
    buf = gs.buffer(d)
    print(f"缓冲距离 {d}: 面积从 {poly.area:.1f} 增加到 {buf.iloc[0].area:.2f}")

14.3.2 负缓冲区(收缩)

负缓冲距离使多边形向内收缩:

poly = Polygon([(0, 0), (10, 0), (10, 8), (0, 8)])
gs = gpd.GeoSeries([poly])

for d in [-0.5, -1.0, -2.0, -5.0]:
    buf = gs.buffer(d)
    if buf.iloc[0].is_empty:
        print(f"缓冲距离 {d}: 几何消失(收缩过度)")
    else:
        print(f"缓冲距离 {d}: 面积从 {poly.area:.1f} 减少到 {buf.iloc[0].area:.2f}")

14.3.3 注意事项

  • 负缓冲区只对面状几何有意义
  • 如果负缓冲距离过大,几何可能完全消失
  • 负缓冲区可用于”腐蚀”操作,清理细小凸出

14.4 不同端头样式 - round、flat、square

14.4.1 cap_style 参数

cap_style 控制线段端头的缓冲区形状:

说明
'round' 半圆形端头(默认)
'flat' 平头,缓冲区在端点处截断
'square' 方形端头,延伸距离等于缓冲距离

14.4.2 代码示例

import geopandas as gpd
from shapely.geometry import LineString

line = LineString([(0, 0), (5, 0)])
gs = gpd.GeoSeries([line])

for style in ['round', 'flat', 'square']:
    buf = gs.buffer(1.0, cap_style=style)
    print(f"cap_style='{style}': 面积={buf.iloc[0].area:.4f}")
# round:  面积 ≈ 13.14 (5×2 + π)
# flat:   面积 ≈ 10.00 (5×2)
# square: 面积 ≈ 14.00 (7×2)

14.5 不同连接样式 - round、mitre、bevel

14.5.1 join_style 参数

join_style 控制缓冲区在折角处的连接方式:

说明
'round' 圆角连接(默认)
'mitre' 尖角连接
'bevel' 斜面连接

14.5.2 代码示例

import geopandas as gpd
from shapely.geometry import LineString

# 直角折线
line = LineString([(0, 0), (2, 0), (2, 2)])
gs = gpd.GeoSeries([line])

for style in ['round', 'mitre', 'bevel']:
    buf = gs.buffer(0.5, join_style=style)
    print(f"join_style='{style}': 面积={buf.iloc[0].area:.4f}")

14.5.3 mitre_limit 参数

当使用 join_style='mitre' 时,mitre_limit 控制尖角的最大延伸距离。如果尖角超过限制,会自动退化为 bevel 连接。

# 锐角折线
sharp_line = LineString([(0, 0), (2, 0), (0, 0.5)])
gs = gpd.GeoSeries([sharp_line])

for limit in [1.0, 2.0, 5.0, 10.0]:
    buf = gs.buffer(0.5, join_style='mitre', mitre_limit=limit)
    print(f"mitre_limit={limit}: 面积={buf.iloc[0].area:.4f}")

14.6 单侧缓冲区 - single_sided 参数

14.6.1 基本概念

single_sided=True 生成线的单侧缓冲区。正距离在线的左侧生成缓冲区,负距离在右侧。

14.6.2 代码示例

import geopandas as gpd
from shapely.geometry import LineString

line = LineString([(0, 0), (5, 0)])
gs = gpd.GeoSeries([line])

# 左侧缓冲区(正距离)
left_buf = gs.buffer(1.0, single_sided=True)
print(f"左侧缓冲区: {left_buf.iloc[0].bounds}")  # y 范围 0-1

# 右侧缓冲区(负距离)
right_buf = gs.buffer(-1.0, single_sided=True)
print(f"右侧缓冲区: {right_buf.iloc[0].bounds}")  # y 范围 -1-0

14.6.3 实际应用

# 生成道路两侧的人行道区域
road = LineString([(0, 0), (100, 0), (100, 50)])
gs = gpd.GeoSeries([road])

# 左侧人行道(宽2米)
left_sidewalk = gs.buffer(2, single_sided=True)

# 右侧人行道(宽2米)
right_sidewalk = gs.buffer(-2, single_sided=True)

14.7 simplify() 详解 - tolerance、preserve_topology

14.7.1 基本概念

simplify() 使用 Douglas-Peucker 算法简化几何对象,减少顶点数量同时尽量保持原始形状。

14.7.2 参数说明

GeoSeries.simplify(tolerance, preserve_topology=True)
参数 说明
tolerance 简化容差,距离阈值
preserve_topology 是否保持拓扑有效性

14.7.3 代码示例

import geopandas as gpd
from shapely.geometry import LineString

# 复杂的曲线
import numpy as np
t = np.linspace(0, 4*np.pi, 200)
x = t
y = np.sin(t)
complex_line = LineString(zip(x, y))

gs = gpd.GeoSeries([complex_line])
print(f"原始顶点数: {len(complex_line.coords)}")

for tol in [0.01, 0.05, 0.1, 0.5, 1.0]:
    simplified = gs.simplify(tol)
    vertices = len(simplified.iloc[0].coords)
    print(f"容差 {tol}: 顶点数={vertices}")

14.7.4 preserve_topology 参数

from shapely.geometry import Polygon

# 复杂多边形
poly = Polygon([(0,0),(1,0),(1,0.5),(2,0.5),(2,0),(3,0),(3,2),(0,2)])
gs = gpd.GeoSeries([poly])

# 保持拓扑(默认)
simplified_topo = gs.simplify(0.6, preserve_topology=True)
print(f"保持拓扑: 有效={simplified_topo.iloc[0].is_valid}")

# 不保持拓扑(可能产生自交叉)
simplified_no_topo = gs.simplify(0.6, preserve_topology=False)
print(f"不保持拓扑: 有效={simplified_no_topo.iloc[0].is_valid}")

14.8 simplify_coverage() - 保持覆盖关系的简化

14.8.1 基本概念

simplify_coverage() 在简化几何的同时保持相邻多边形之间的拓扑关系(共享边界一致性)。这对于行政区划等覆盖数据特别重要。

14.8.2 代码示例

import geopandas as gpd
from shapely.geometry import Polygon

# 两个相邻多边形
gs = gpd.GeoSeries([
    Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
    Polygon([(1, 0), (2, 0), (2, 1), (1, 1)]),
])

# 覆盖简化
simplified = gs.simplify_coverage(tolerance=0.1)
# 共享边会被一致地简化,不会产生间隙或重叠

14.9 offset_curve() - 平行偏移线

14.9.1 基本概念

offset_curve() 生成与原始线平行偏移的线。正距离向左偏移,负距离向右偏移。

14.9.2 代码示例

import geopandas as gpd
from shapely.geometry import LineString

line = LineString([(0, 0), (5, 0), (5, 5)])
gs = gpd.GeoSeries([line])

# 左偏移
left_offset = gs.offset_curve(1.0)
print(f"左偏移: {left_offset.iloc[0]}")

# 右偏移
right_offset = gs.offset_curve(-1.0)
print(f"右偏移: {right_offset.iloc[0]}")

14.9.3 参数选项

# join_style 控制折角处理
for style in ['round', 'mitre', 'bevel']:
    offset = gs.offset_curve(1.0, join_style=style)
    print(f"join_style='{style}': {offset.iloc[0].geom_type}")

14.9.4 实际应用

# 生成道路标线
road_center = LineString([(0, 0), (100, 0)])
gs = gpd.GeoSeries([road_center])

# 车道分割线
lane_marking = gs.offset_curve(1.75)  # 单车道宽3.5m,中心偏移1.75m

14.10 segmentize() - 添加顶点到几何体

14.10.1 基本概念

segmentize() 在几何对象上添加额外的顶点,确保任意两个相邻顶点之间的距离不超过指定值。

14.10.2 代码示例

import geopandas as gpd
from shapely.geometry import LineString

# 只有两个顶点的长线段
line = LineString([(0, 0), (100, 0)])
gs = gpd.GeoSeries([line])

print(f"原始顶点数: {len(line.coords)}")  # 2

# 最大间距 10
segmented = gs.segmentize(max_segment_length=10)
print(f"分段后顶点数: {len(segmented.iloc[0].coords)}")  # 11

# 最大间距 25
segmented2 = gs.segmentize(max_segment_length=25)
print(f"分段后顶点数: {len(segmented2.iloc[0].coords)}")  # 5

14.10.3 实际应用

# 坐标转换前增加顶点密度以提高精度
import geopandas as gpd

# 大范围线(如国界线)在投影转换时可能变形
boundaries = gpd.read_file("boundaries.shp")

# 在转换前增加顶点密度
dense = boundaries.copy()
dense['geometry'] = boundaries.segmentize(max_segment_length=0.1)  # 度

# 然后进行投影转换
dense_proj = dense.to_crs(epsg=32650)

14.11 remove_repeated_points() - 移除重复坐标

14.11.1 基本概念

remove_repeated_points() 移除几何对象中距离在指定容差内的重复顶点。

14.11.2 代码示例

import geopandas as gpd
from shapely.geometry import LineString

# 包含近似重复点的线
line = LineString([
    (0, 0), (1, 0), (1.001, 0.001),  # 第3个点与第2个点很近
    (2, 0), (3, 0), (3, 0.0001),      # 第6个点与第5个点几乎重合
    (4, 0)
])

gs = gpd.GeoSeries([line])
print(f"原始顶点数: {len(line.coords)}")  # 7

# 移除容差 0.01 内的重复点
cleaned = gs.remove_repeated_points(tolerance=0.01)
print(f"清理后顶点数: {len(cleaned.iloc[0].coords)}")

14.11.3 实际应用

# 清理 GPS 轨迹中的停留点(几乎不动的点)
tracks = gpd.read_file("gps_tracks.shp")

# 移除 1 米内的重复点
cleaned = tracks.copy()
cleaned['geometry'] = tracks.remove_repeated_points(tolerance=1.0)

original_count = sum(len(g.coords) for g in tracks.geometry)
cleaned_count = sum(len(g.coords) for g in cleaned.geometry)
print(f"从 {original_count} 个点减少到 {cleaned_count} 个点")

14.12 缓冲区在GIS分析中的应用

14.12.1 服务区分析

import geopandas as gpd
from shapely.geometry import Point

# 医院位置
hospitals = gpd.GeoDataFrame({
    'name': ['市第一医院', '市第二医院', '市第三医院'],
    'level': ['三甲', '三乙', '二甲'],
    'geometry': [Point(500000, 3500000), Point(502000, 3501000), Point(498000, 3499000)]
}, crs='EPSG:32650')

# 不同等级医院不同服务半径
service_radius = {'三甲': 5000, '三乙': 3000, '二甲': 2000}

hospitals['service_area'] = hospitals.apply(
    lambda row: row.geometry.buffer(service_radius[row['level']]),
    axis=1
)

# 计算服务覆盖率
service_gdf = hospitals.set_geometry('service_area')
total_coverage = service_gdf.union_all()
print(f"总服务覆盖面积: {total_coverage.area/1e6:.2f} 平方公里")

14.12.2 多环缓冲区

# 生成多环缓冲区(等值线效果)
center = Point(500000, 3500000)

rings = []
distances = [1000, 2000, 3000, 5000, 8000]

for i, d in enumerate(distances):
    if i == 0:
        ring = center.buffer(d)
    else:
        ring = center.buffer(d).difference(center.buffer(distances[i-1]))
    rings.append({'distance': d, 'geometry': ring})

ring_gdf = gpd.GeoDataFrame(rings, crs='EPSG:32650')

14.12.3 影响范围分析

# 分析工厂污染影响范围
factories = gpd.read_file("factories.shp")
residential = gpd.read_file("residential.shp")

# 500米影响范围
impact_zones = factories.buffer(500)

# 查找受影响的居民区
affected = residential[residential.intersects(impact_zones.union_all())]
print(f"受影响居民区: {len(affected)} 个")

14.13 简化的质量评估

14.13.1 评估指标

import geopandas as gpd
from shapely.geometry import Polygon
import numpy as np

poly = Polygon([(0,0),(1,0.1),(2,0),(2.1,1),(2,2),(1,1.9),(0,2),(-0.1,1)])
gs = gpd.GeoSeries([poly])

for tol in [0.05, 0.1, 0.2, 0.5]:
    simplified = gs.simplify(tol)
    orig = gs.iloc[0]
    simp = simplified.iloc[0]

    # 面积变化率
    area_change = abs(simp.area - orig.area) / orig.area * 100

    # Hausdorff 距离
    hausdorff = orig.hausdorff_distance(simp)

    # 顶点减少率
    orig_pts = len(orig.exterior.coords)
    simp_pts = len(simp.exterior.coords)
    reduction = (1 - simp_pts / orig_pts) * 100

    print(f"容差 {tol}: 面积变化={area_change:.2f}%, "
          f"Hausdorff={hausdorff:.4f}, 顶点减少={reduction:.1f}%")

14.13.2 选择合适的容差

应用场景 建议容差 说明
高精度分析 数据精度的 1-2 倍 最小简化
Web 地图显示 像素大小的 1-2 倍 根据缩放级别调整
缩略图/概览 较大容差 大幅简化
面积计算 小容差 保持面积精度

14.14 本章小结

本章全面介绍了 GeoPandas 中的缓冲区与简化操作。主要内容回顾:

缓冲区操作

操作 说明
buffer(d) 基本缓冲区,d>0 膨胀,d<0 收缩
cap_style 线端头样式:round、flat、square
join_style 折角连接:round、mitre、bevel
single_sided 单侧缓冲区

简化与变换操作

操作 说明
simplify() Douglas-Peucker 简化
simplify_coverage() 保持覆盖关系的简化
offset_curve() 平行偏移线
segmentize() 增加顶点密度
remove_repeated_points() 移除重复坐标

使用建议

  1. 坐标系选择:缓冲距离单位取决于 CRS,建议使用投影坐标系
  2. resolution 选择:Web 显示可用低值(4-8),精确分析用高值(32+)
  3. 简化容差:根据应用场景和显示比例尺选择
  4. 拓扑保持:多边形简化时建议 preserve_topology=True
  5. 负缓冲区:可用于清理细小凸出和腐蚀操作

下一章我们将学习空间索引与查询优化,探索如何提高大数据集上空间操作的性能。