znlgis 博客

GIS开发与技术分享

第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 从线构建多边形

下一章将介绍空间度量与计算,学习距离、面积、长度等测量函数。