第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() |
移除重复坐标 |
使用建议
- 坐标系选择:缓冲距离单位取决于 CRS,建议使用投影坐标系
- resolution 选择:Web 显示可用低值(4-8),精确分析用高值(32+)
- 简化容差:根据应用场景和显示比例尺选择
- 拓扑保持:多边形简化时建议
preserve_topology=True - 负缓冲区:可用于清理细小凸出和腐蚀操作
下一章我们将学习空间索引与查询优化,探索如何提高大数据集上空间操作的性能。