第7章:构造性操作
构造性操作是指从现有几何对象生成新几何对象的操作。Shapely 提供了丰富的构造性操作,包括 缓冲区分析、凸包、凹包、包络矩形、简化、质心计算、几何修复等。这些操作是空间分析和 GIS 应用中最常用的功能之一。本章将全面介绍每种构造性操作的原理、参数和使用方法。
buffer:缓冲区分析
基本概念
缓冲区(Buffer)是最常用的空间分析操作之一。对于给定的几何对象和距离 d,缓冲区操作返回 距离该几何对象不超过 d 的所有点组成的区域。
- 正距离:向外扩展(膨胀)
- 负距离:向内收缩(仅对面状几何有意义)
- 零距离:可用于修复某些几何问题
基本用法
from shapely import buffer
from shapely.geometry import Point, LineString, Polygon
# 点的缓冲区 -> 圆
point = Point(0, 0)
circle = buffer(point, 5)
print(f"圆面积: {circle.area:.2f}") # ≈ 78.54
# 线的缓冲区 -> 带状区域
line = LineString([(0, 0), (10, 0)])
corridor = buffer(line, 2)
print(f"走廊面积: {corridor.area:.2f}")
# 多边形的正缓冲 -> 扩展
poly = Polygon([(0, 0), (4, 0), (4, 3), (0, 3)])
expanded = buffer(poly, 1)
print(f"扩展后面积: {expanded.area:.2f}")
负距离缓冲(内缩)
from shapely import buffer
from shapely.geometry import Polygon
poly = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
print(f"原始面积: {poly.area}")
shrunk = buffer(poly, -2)
print(f"内缩后面积: {shrunk.area}") # 36.0 (6x6)
# 过度内缩会导致空几何
vanished = buffer(poly, -6)
print(f"过度内缩: {vanished.is_empty}") # True
resolution / quad_segs 参数
控制圆弧的精度(每个 90 度弧的线段数量),默认值为 16:
from shapely import buffer
from shapely.geometry import Point
point = Point(0, 0)
# 不同精度
for segs in [4, 8, 16, 32, 64]:
buf = buffer(point, 1, quad_segs=segs)
n_coords = len(buf.exterior.coords)
print(f"quad_segs={segs:2d}: 顶点数={n_coords:3d}, 面积={buf.area:.6f}")
cap_style:端点样式
控制线缓冲区端点的形状:
from shapely import buffer
from shapely.geometry import LineString
line = LineString([(0, 0), (5, 0)])
# 圆形端点(默认)
buf_round = buffer(line, 1, cap_style="round")
print(f"round 面积: {buf_round.area:.2f}")
# 平头端点
buf_flat = buffer(line, 1, cap_style="flat")
print(f"flat 面积: {buf_flat.area:.2f}")
# 方形端点
buf_square = buffer(line, 1, cap_style="square")
print(f"square 面积: {buf_square.area:.2f}")
cap_style 可选值:
| 值 | 说明 |
|---|---|
"round" |
圆形(默认),端点处绘制半圆 |
"flat" |
平头,缓冲区在端点处截止 |
"square" |
方形,端点处绘制方形延伸 |
join_style:拐角样式
控制缓冲区在拐角处的形状:
from shapely import buffer
from shapely.geometry import LineString
# 带拐角的折线
line = LineString([(0, 0), (3, 0), (3, 3)])
# 圆角拐角(默认)
buf_round = buffer(line, 1, join_style="round")
print(f"round 面积: {buf_round.area:.2f}")
# 斜接拐角
buf_mitre = buffer(line, 1, join_style="mitre")
print(f"mitre 面积: {buf_mitre.area:.2f}")
# 斜切拐角
buf_bevel = buffer(line, 1, join_style="bevel")
print(f"bevel 面积: {buf_bevel.area:.2f}")
mitre_ratio 参数
当 join_style="mitre" 时,mitre_ratio 限制斜接角的最大长度(相对于缓冲距离的比值):
from shapely import buffer
from shapely.geometry import LineString
# 尖角折线
line = LineString([(0, 0), (5, 0), (5.1, 5)])
for ratio in [1.0, 2.0, 5.0, 10.0]:
buf = buffer(line, 0.5, join_style="mitre", mitre_limit=ratio)
print(f"mitre_limit={ratio:.1f}: 面积={buf.area:.2f}")
single_sided:单侧缓冲
仅在几何对象的一侧生成缓冲区(仅对线状几何有效):
from shapely import buffer
from shapely.geometry import LineString
line = LineString([(0, 0), (10, 0)])
# 左侧缓冲(正距离)
buf_left = buffer(line, 2, single_sided=True)
print(f"左侧缓冲面积: {buf_left.area:.2f}")
# 右侧缓冲(负距离)
buf_right = buffer(line, -2, single_sided=True)
print(f"右侧缓冲面积: {buf_right.area:.2f}")
convex_hull:凸包
凸包是包含给定几何对象的最小凸多边形,类似于将一根橡皮筋套在所有点的外围。
from shapely import convex_hull
from shapely.geometry import MultiPoint, Polygon
# 从散点集计算凸包
points = MultiPoint([(0, 0), (1, 1), (2, 0.5), (3, 2), (0.5, 3), (1, 2)])
hull = convex_hull(points)
print(f"凸包类型: {hull.geom_type}")
print(f"凸包面积: {hull.area:.2f}")
print(f"凸包顶点数: {len(hull.exterior.coords) - 1}")
from shapely import convex_hull
from shapely.geometry import Polygon
# 凹多边形的凸包
concave = Polygon([(0, 0), (4, 0), (4, 3), (2, 1), (0, 3)])
hull = convex_hull(concave)
print(f"原始面积: {concave.area}")
print(f"凸包面积: {hull.area}")
concave_hull:凹包
凹包(Shapely 2.1+)生成更紧密贴合点集的包围多边形。通过 ratio 参数控制凹陷程度。
from shapely import concave_hull
from shapely.geometry import MultiPoint
points = MultiPoint([
(0, 0), (1, 0), (2, 0), (3, 0),
(0, 1), (3, 1),
(0, 2), (3, 2),
(0, 3), (1, 3), (2, 3), (3, 3),
(1.5, 1.5),
])
# ratio=0: 最凹(最紧密)
hull_tight = concave_hull(points, ratio=0)
print(f"ratio=0 面积: {hull_tight.area:.2f}")
# ratio=0.5: 中等
hull_mid = concave_hull(points, ratio=0.5)
print(f"ratio=0.5 面积: {hull_mid.area:.2f}")
# ratio=1: 等同于凸包
hull_convex = concave_hull(points, ratio=1.0)
print(f"ratio=1.0 面积: {hull_convex.area:.2f}")
allow_holes 参数
from shapely import concave_hull
from shapely.geometry import MultiPoint
# 中空分布的点
points = MultiPoint([
(0, 0), (1, 0), (2, 0), (3, 0), (4, 0),
(0, 1), (4, 1),
(0, 2), (4, 2),
(0, 3), (4, 3),
(0, 4), (1, 4), (2, 4), (3, 4), (4, 4),
])
# 不允许洞
hull_no_holes = concave_hull(points, ratio=0.3, allow_holes=False)
print(f"无洞内环数: {len(list(hull_no_holes.interiors))}")
# 允许洞
hull_with_holes = concave_hull(points, ratio=0.3, allow_holes=True)
print(f"有洞内环数: {len(list(hull_with_holes.interiors))}")
envelope:最小外接矩形(轴对齐)
envelope 返回几何对象的最小轴对齐外接矩形(Bounding Box)。
from shapely import envelope
from shapely.geometry import Polygon, MultiPoint
poly = Polygon([(1, 2), (3, 1), (5, 4), (2, 5)])
env = envelope(poly)
print(f"外接矩形: {env}")
print(f"外接矩形面积: {env.area}")
# 对散点集
points = MultiPoint([(0, 0), (3, 1), (2, 4), (5, 2)])
env_pts = envelope(points)
print(f"点集外接矩形: {env_pts}")
minimum_rotated_rectangle / oriented_envelope:最小旋转外接矩形
返回面积最小的外接矩形,可以是任意角度的旋转矩形。
from shapely import minimum_rotated_rectangle, oriented_envelope
from shapely.geometry import MultiPoint
points = MultiPoint([(0, 0), (4, 1), (3, 4), (1, 3)])
# 两种方式(等价)
rect1 = minimum_rotated_rectangle(points)
rect2 = oriented_envelope(points)
print(f"最小旋转矩形面积: {rect1.area:.2f}")
print(f"坐标: {list(rect1.exterior.coords)}")
# 与轴对齐矩形对比
from shapely import envelope
env = envelope(points)
print(f"轴对齐矩形面积: {env.area:.2f}")
print(f"面积节省: {(1 - rect1.area / env.area) * 100:.1f}%")
minimum_bounding_circle:最小外接圆
from shapely import minimum_bounding_circle
from shapely.geometry import MultiPoint
points = MultiPoint([(0, 0), (4, 0), (2, 3)])
circle = minimum_bounding_circle(points)
print(f"最小外接圆面积: {circle.area:.2f}")
print(f"圆心: ({circle.centroid.x:.2f}, {circle.centroid.y:.2f})")
simplify:几何简化
基本用法
simplify 使用 Douglas-Peucker 算法简化几何对象,减少顶点数量。
from shapely import simplify
from shapely.geometry import LineString
# 创建一条含有许多细节的线
import math
coords = [(i * 0.1, math.sin(i * 0.1)) for i in range(100)]
line = LineString(coords)
print(f"原始顶点数: {len(line.coords)}")
# 不同容差简化
for tol in [0.01, 0.05, 0.1, 0.5]:
simplified = simplify(line, tol)
print(f"tolerance={tol}: 顶点数={len(simplified.coords)}")
preserve_topology 参数
from shapely import simplify
from shapely.geometry import Polygon
# 复杂多边形
poly = Polygon([
(0, 0), (1, 0.1), (2, 0), (3, 0.1), (4, 0),
(4, 1), (3.9, 2), (4, 3),
(3, 2.9), (2, 3), (1, 2.9), (0, 3),
(0.1, 2), (0, 1),
])
# preserve_topology=True(默认):保证结果有效
s1 = simplify(poly, 0.5, preserve_topology=True)
print(f"保持拓扑: 有效={s1.is_valid}, 顶点数={len(s1.exterior.coords)}")
# preserve_topology=False:可能产生无效结果但更简化
s2 = simplify(poly, 0.5, preserve_topology=False)
print(f"不保持拓扑: 有效={s2.is_valid}, 顶点数={len(s2.exterior.coords)}")
remove_repeated_points:移除重复点
from shapely import remove_repeated_points
from shapely.geometry import LineString
# 包含重复点的线
line = LineString([(0, 0), (1, 0), (1, 0), (2, 0), (2, 0), (2, 0), (3, 0)])
print(f"原始顶点数: {len(line.coords)}")
cleaned = remove_repeated_points(line)
print(f"清理后顶点数: {len(cleaned.coords)}")
# 带容差
line2 = LineString([(0, 0), (0.001, 0.001), (1, 0), (1.002, 0.001), (2, 0)])
cleaned2 = remove_repeated_points(line2, tolerance=0.01)
print(f"带容差清理顶点数: {len(cleaned2.coords)}")
segmentize:加密顶点
在几何对象的边上插入顶点,使得每条线段的长度不超过指定值。
from shapely import segmentize
from shapely.geometry import LineString
line = LineString([(0, 0), (10, 0)])
print(f"原始顶点数: {len(line.coords)}")
# 每段最长 2 个单位
dense = segmentize(line, max_segment_length=2)
print(f"加密后顶点数: {len(dense.coords)}")
print(f"坐标: {list(dense.coords)}")
from shapely import segmentize
from shapely.geometry import Polygon
# 多边形边加密
poly = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
print(f"原始顶点数: {len(poly.exterior.coords)}")
dense_poly = segmentize(poly, max_segment_length=2)
print(f"加密后顶点数: {len(dense_poly.exterior.coords)}")
centroid:质心
from shapely import centroid
from shapely.geometry import Polygon, LineString, MultiPoint
# 多边形质心
poly = Polygon([(0, 0), (4, 0), (4, 3), (0, 3)])
c = centroid(poly)
print(f"矩形质心: ({c.x}, {c.y})") # (2, 1.5)
# 三角形质心
tri = Polygon([(0, 0), (6, 0), (3, 6)])
c_tri = centroid(tri)
print(f"三角形质心: ({c_tri.x}, {c_tri.y})")
# 线质心
line = LineString([(0, 0), (10, 0)])
c_line = centroid(line)
print(f"线质心: ({c_line.x}, {c_line.y})") # (5, 0)
注意:质心不一定在几何对象的内部(例如凹多边形或环形区域)。
point_on_surface:保证在面上的点
与 centroid 不同,point_on_surface 保证返回的点位于几何对象的内部或边界上。
from shapely import centroid, point_on_surface
from shapely.geometry import Polygon
# L 形多边形(凹多边形)
L_shape = Polygon([
(0, 0), (3, 0), (3, 1), (1, 1), (1, 3), (0, 3)
])
c = centroid(L_shape)
p = point_on_surface(L_shape)
print(f"质心: ({c.x:.2f}, {c.y:.2f}), 在面上: {L_shape.contains(c)}")
print(f"面上点: ({p.x:.2f}, {p.y:.2f}), 在面上: {L_shape.contains(p)}")
boundary:边界
from shapely import boundary
from shapely.geometry import Polygon, LineString, Point
# 多边形边界 -> 线
poly = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
b_poly = boundary(poly)
print(f"多边形边界类型: {b_poly.geom_type}") # LinearRing
# 线的边界 -> 端点
line = LineString([(0, 0), (1, 1), (2, 0)])
b_line = boundary(line)
print(f"线边界类型: {b_line.geom_type}") # MultiPoint
# 点的边界 -> 空几何
point = Point(0, 0)
b_point = boundary(point)
print(f"点边界为空: {b_point.is_empty}") # True
extract_unique_points:提取唯一顶点
from shapely import extract_unique_points
from shapely.geometry import Polygon
poly = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
points = extract_unique_points(poly)
print(f"唯一顶点类型: {points.geom_type}")
print(f"唯一顶点数量: {len(points.geoms)}")
for pt in points.geoms:
print(f" ({pt.x}, {pt.y})")
reverse:反转坐标顺序
from shapely import reverse
from shapely.geometry import LineString
line = LineString([(0, 0), (1, 1), (2, 0)])
print(f"原始: {list(line.coords)}")
rev = reverse(line)
print(f"反转: {list(rev.coords)}")
normalize:规范化形式
将几何对象转换为规范化形式,用于确定性比较。
from shapely import normalize
from shapely.geometry import Polygon
# 两个顶点顺序不同但几何相同的多边形
p1 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
p2 = Polygon([(1, 1), (0, 1), (0, 0), (1, 0)])
n1 = normalize(p1)
n2 = normalize(p2)
print(f"规范化后 WKT 相同: {n1.wkt == n2.wkt}")
orient_polygons:统一方向
统一多边形环的方向(外环逆时针 CCW,内环顺时针 CW)。
from shapely.geometry import Polygon
from shapely import is_ccw
# 顺时针多边形
cw_poly = Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])
ring = cw_poly.exterior
print(f"原始方向是否逆时针: {is_ccw(ring)}")
# 使用 orient 统一为逆时针
from shapely.geometry.polygon import orient
oriented = orient(cw_poly, sign=1.0)
ring2 = oriented.exterior
print(f"调整后是否逆时针: {is_ccw(ring2)}")
clip_by_rect:矩形裁剪
快速将几何对象裁剪到给定矩形范围内。比使用 intersection 配合矩形更快,但结果可能不精确。
from shapely import clip_by_rect
from shapely.geometry import Polygon, LineString
poly = Polygon([(0, 0), (5, 0), (5, 5), (0, 5)])
clipped = clip_by_rect(poly, 1, 1, 3, 3) # xmin, ymin, xmax, ymax
print(f"裁剪后面积: {clipped.area}") # 4.0
# 裁剪线
line = LineString([(0, 0), (5, 5)])
clipped_line = clip_by_rect(line, 1, 1, 3, 3)
print(f"裁剪后线: {clipped_line}")
make_valid:修复无效几何
from shapely import make_valid
from shapely.geometry import Polygon
# 自相交多边形(蝴蝶结)
bowtie = Polygon([(0, 0), (2, 2), (2, 0), (0, 2)])
print(f"有效: {bowtie.is_valid}")
fixed = make_valid(bowtie)
print(f"修复后有效: {fixed.is_valid}")
print(f"修复后类型: {fixed.geom_type}")
print(f"修复后面积: {fixed.area:.2f}")
from shapely import make_valid
from shapely.geometry import Polygon
# 带有重叠环的无效多边形
invalid_poly = Polygon(
[(0, 0), (10, 0), (10, 10), (0, 10)],
[[(2, 2), (8, 2), (8, 8), (2, 8)]] # 内环
)
# 翻转内环方向使其无效
from shapely import reverse
inner_ring = invalid_poly.interiors[0]
print(f"原始多边形有效: {invalid_poly.is_valid}")
fixed = make_valid(invalid_poly)
print(f"修复后有效: {fixed.is_valid}")
node:在交点处打断
将线段在交叉点处打断为多个线段。
from shapely import node
from shapely.geometry import MultiLineString
lines = MultiLineString([
[(0, 0), (2, 2)],
[(0, 2), (2, 0)],
])
noded = node(lines)
print(f"打断后类型: {noded.geom_type}")
print(f"打断后线段数: {len(noded.geoms)}")
for i, g in enumerate(noded.geoms):
print(f" 线段{i}: {list(g.coords)}")
build_area:从线段构建多边形
from shapely import build_area
from shapely.geometry import GeometryCollection, LineString
# 构建闭合区域
lines = GeometryCollection([
LineString([(0, 0), (5, 0)]),
LineString([(5, 0), (5, 5)]),
LineString([(5, 5), (0, 5)]),
LineString([(0, 5), (0, 0)]),
])
area = build_area(lines)
print(f"构建结果类型: {area.geom_type}")
print(f"构建结果面积: {area.area}")
snap:捕捉
将 geom1 中接近 geom2 的顶点捕捉到 geom2 上。
from shapely import snap
from shapely.geometry import Polygon, LineString
poly = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
line = LineString([(0.1, -0.1), (9.9, -0.1)])
# 容差 0.5 内的点会被捕捉
snapped = snap(poly, line, tolerance=0.5)
print(f"捕捉后外环: {list(snapped.exterior.coords)[:3]}...")
polygonize / polygonize_full:从线构建多边形
polygonize
从一组线段构建所有可能的多边形:
from shapely.ops import polygonize
from shapely.geometry import LineString
lines = [
LineString([(0, 0), (4, 0)]),
LineString([(4, 0), (4, 4)]),
LineString([(4, 4), (0, 4)]),
LineString([(0, 4), (0, 0)]),
LineString([(2, 0), (2, 4)]), # 中线
]
polygons = list(polygonize(lines))
print(f"构建了 {len(polygons)} 个多边形")
for i, p in enumerate(polygons):
print(f" 多边形{i}: 面积={p.area}")
polygonize_full
返回更详细的结果,包括无法构建多边形的悬挂线和裁剪边:
from shapely.ops import polygonize_full
from shapely.geometry import LineString
lines = [
LineString([(0, 0), (4, 0)]),
LineString([(4, 0), (4, 4)]),
LineString([(4, 4), (0, 4)]),
LineString([(0, 4), (0, 0)]),
LineString([(5, 0), (6, 0)]), # 悬挂线
]
result, dangles, cuts, invalids = polygonize_full(lines)
print(f"有效多边形: {len(result.geoms)}")
print(f"悬挂线: {len(dangles.geoms)}")
print(f"裁剪边: {len(cuts.geoms)}")
print(f"无效环: {len(invalids.geoms)}")
每种操作的综合示例
综合示例 1:建筑物分析
from shapely import buffer, convex_hull, centroid, simplify
from shapely.geometry import Polygon
# 模拟一个不规则建筑轮廓
building = Polygon([
(0, 0), (5, 0), (5, 3), (8, 3), (8, 7),
(5, 7), (5, 10), (0, 10), (0, 7), (-2, 7),
(-2, 3), (0, 3),
])
# 基本属性
c = centroid(building)
print(f"建筑面积: {building.area}")
print(f"建筑质心: ({c.x:.1f}, {c.y:.1f})")
# 5 米安全缓冲区
safety_zone = buffer(building, 5)
print(f"安全区面积: {safety_zone.area:.1f}")
# 凸包
hull = convex_hull(building)
print(f"凸包面积: {hull.area}")
print(f"凹凸比: {building.area / hull.area:.2f}")
# 简化
simplified = simplify(building, 1.0)
print(f"简化后顶点数: {len(simplified.exterior.coords)}")
综合示例 2:道路缓冲区样式对比
from shapely import buffer
from shapely.geometry import LineString
road = LineString([(0, 0), (5, 0), (5, 5), (10, 5)])
styles = [
{"cap_style": "round", "join_style": "round"},
{"cap_style": "flat", "join_style": "mitre"},
{"cap_style": "square", "join_style": "bevel"},
]
for style in styles:
buf = buffer(road, 1, **style)
print(f"cap={style['cap_style']:6s}, join={style['join_style']:5s} -> "
f"面积={buf.area:.2f}, 顶点数={len(buf.exterior.coords)}")
综合示例 3:多边形修复与简化流程
from shapely import make_valid, simplify, buffer
from shapely.geometry import Polygon
# 自相交多边形
invalid = Polygon([(0, 0), (4, 4), (4, 0), (0, 4)])
# 步骤1: 修复
valid = make_valid(invalid)
print(f"修复后: {valid.geom_type}, 有效: {valid.is_valid}")
# 步骤2: 0距离缓冲(进一步清理)
cleaned = buffer(valid, 0)
print(f"清理后: {cleaned.geom_type}")
# 步骤3: 简化
final = simplify(cleaned, 0.1)
print(f"最终: 有效={final.is_valid}, 面积={final.area:.2f}")
本章小结
本章介绍了 Shapely 的所有构造性操作:
| 操作 | 说明 |
|---|---|
buffer |
缓冲区分析(膨胀/收缩) |
convex_hull |
凸包 |
concave_hull |
凹包 |
envelope |
轴对齐外接矩形 |
minimum_rotated_rectangle |
最小旋转外接矩形 |
minimum_bounding_circle |
最小外接圆 |
simplify |
Douglas-Peucker 简化 |
segmentize |
顶点加密 |
centroid |
质心 |
point_on_surface |
面内点 |
boundary |
边界 |
clip_by_rect |
矩形裁剪 |
make_valid |
修复无效几何 |
snap |
捕捉 |
polygonize |
从线构建多边形 |
下一章将介绍空间度量与计算,学习距离、面积、长度等测量函数。