Skip to content

Commit 6f41d92

Browse files
committed
Add polygonize simplification user guide notebook (#1151)
1 parent e073e99 commit 6f41d92

File tree

1 file changed

+213
-0
lines changed

1 file changed

+213
-0
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Polygonize with Geometry Simplification\n",
8+
"\n",
9+
"`polygonize()` converts raster regions into vector polygons. On high-resolution rasters the result can have thousands of vertices per polygon, which slows rendering and inflates file size.\n",
10+
"\n",
11+
"The `simplify_tolerance` parameter simplifies polygon boundaries during polygonization. Two algorithms are available:\n",
12+
"- `simplify_method=\"douglas-peucker\"` (default): distance-based, removes vertices that deviate less than the tolerance from the simplified line\n",
13+
"- `simplify_method=\"visvalingam-whyatt\"`: area-based, removes vertices that form triangles smaller than the tolerance threshold\n",
14+
"\n",
15+
"Adjacent polygons share identical simplified boundaries, so no gaps or overlaps are introduced."
16+
]
17+
},
18+
{
19+
"cell_type": "code",
20+
"execution_count": null,
21+
"metadata": {},
22+
"outputs": [],
23+
"source": [
24+
"import numpy as np\n",
25+
"import xarray as xr\n",
26+
"import matplotlib.pyplot as plt\n",
27+
"from matplotlib.patches import Polygon as MplPolygon\n",
28+
"from matplotlib.collections import PatchCollection\n",
29+
"\n",
30+
"from xrspatial import polygonize"
31+
]
32+
},
33+
{
34+
"cell_type": "markdown",
35+
"metadata": {},
36+
"source": [
37+
"## Generate a classified raster\n",
38+
"\n",
39+
"A synthetic land-cover raster with irregular region boundaries."
40+
]
41+
},
42+
{
43+
"cell_type": "code",
44+
"execution_count": null,
45+
"metadata": {},
46+
"outputs": [],
47+
"source": [
48+
"rng = np.random.default_rng(42)\n",
49+
"shape = (80, 120)\n",
50+
"\n",
51+
"from scipy.ndimage import gaussian_filter\n",
52+
"noise = rng.standard_normal(shape)\n",
53+
"smooth = gaussian_filter(noise, sigma=8)\n",
54+
"classified = np.digitize(smooth, bins=[-0.5, 0.0, 0.5]) + 1\n",
55+
"\n",
56+
"raster = xr.DataArray(classified.astype(np.int32))\n",
57+
"\n",
58+
"fig, ax = plt.subplots(figsize=(10, 6))\n",
59+
"raster.plot(ax=ax, cmap=\"Set2\", add_colorbar=True)\n",
60+
"ax.set_title(\"Classified raster (4 land-cover types)\")\n",
61+
"ax.set_aspect(\"equal\")\n",
62+
"plt.tight_layout()\n",
63+
"plt.show()"
64+
]
65+
},
66+
{
67+
"cell_type": "markdown",
68+
"metadata": {},
69+
"source": [
70+
"## Polygonize without simplification"
71+
]
72+
},
73+
{
74+
"cell_type": "code",
75+
"execution_count": null,
76+
"metadata": {},
77+
"outputs": [],
78+
"source": [
79+
"col_raw, pp_raw = polygonize(raster)\n",
80+
"total_verts_raw = sum(len(r) for rings in pp_raw for r in rings)\n",
81+
"print(f\"Polygons: {len(pp_raw)}, Total vertices: {total_verts_raw}\")"
82+
]
83+
},
84+
{
85+
"cell_type": "code",
86+
"execution_count": null,
87+
"metadata": {},
88+
"outputs": [],
89+
"source": [
90+
"def plot_polygons(polygon_points, column, title, ax):\n",
91+
" cmap = plt.cm.Set2\n",
92+
" vals = sorted(set(column))\n",
93+
" val_to_color = {v: cmap(i / max(len(vals) - 1, 1)) for i, v in enumerate(vals)}\n",
94+
"\n",
95+
" patches = []\n",
96+
" colors = []\n",
97+
" for val, rings in zip(column, polygon_points):\n",
98+
" ext = rings[0]\n",
99+
" patches.append(MplPolygon(ext[:, :2], closed=True))\n",
100+
" colors.append(val_to_color[val])\n",
101+
"\n",
102+
" pc = PatchCollection(patches, facecolors=colors, edgecolors=\"black\",\n",
103+
" linewidths=0.3)\n",
104+
" ax.add_collection(pc)\n",
105+
" ax.set_xlim(0, 120)\n",
106+
" ax.set_ylim(0, 80)\n",
107+
" ax.set_aspect(\"equal\")\n",
108+
" ax.set_title(title)\n",
109+
" ax.invert_yaxis()\n",
110+
"\n",
111+
"fig, ax = plt.subplots(figsize=(10, 6))\n",
112+
"plot_polygons(pp_raw, col_raw, f\"Raw polygons ({total_verts_raw} vertices)\", ax)\n",
113+
"plt.tight_layout()\n",
114+
"plt.show()"
115+
]
116+
},
117+
{
118+
"cell_type": "markdown",
119+
"metadata": {},
120+
"source": [
121+
"## Douglas-Peucker simplification\n",
122+
"\n",
123+
"Increasing tolerance values show the trade-off between fidelity and vertex count. The tolerance is in coordinate units (pixels here)."
124+
]
125+
},
126+
{
127+
"cell_type": "code",
128+
"execution_count": null,
129+
"metadata": {},
130+
"outputs": [],
131+
"source": [
132+
"fig, axes = plt.subplots(1, 3, figsize=(18, 6))\n",
133+
"\n",
134+
"for ax, tol in zip(axes, [0.5, 1.5, 3.0]):\n",
135+
" col, pp = polygonize(raster, simplify_tolerance=tol)\n",
136+
" n_verts = sum(len(r) for rings in pp for r in rings)\n",
137+
" reduction = 100 * (1 - n_verts / total_verts_raw)\n",
138+
" plot_polygons(pp, col,\n",
139+
" f\"tolerance={tol} ({n_verts} verts, {reduction:.0f}% reduction)\",\n",
140+
" ax)\n",
141+
"\n",
142+
"plt.suptitle(\"Douglas-Peucker simplification\", fontsize=14, y=1.02)\n",
143+
"plt.tight_layout()\n",
144+
"plt.show()"
145+
]
146+
},
147+
{
148+
"cell_type": "markdown",
149+
"metadata": {},
150+
"source": [
151+
"## Visvalingam-Whyatt simplification\n",
152+
"\n",
153+
"Area-based simplification removes vertices that contribute the least area change. The tolerance is a minimum triangle area threshold."
154+
]
155+
},
156+
{
157+
"cell_type": "code",
158+
"execution_count": null,
159+
"metadata": {},
160+
"outputs": [],
161+
"source": [
162+
"fig, axes = plt.subplots(1, 3, figsize=(18, 6))\n",
163+
"\n",
164+
"for ax, tol in zip(axes, [0.25, 1.0, 3.0]):\n",
165+
" col, pp = polygonize(raster, simplify_tolerance=tol,\n",
166+
" simplify_method=\"visvalingam-whyatt\")\n",
167+
" n_verts = sum(len(r) for rings in pp for r in rings)\n",
168+
" reduction = 100 * (1 - n_verts / total_verts_raw)\n",
169+
" plot_polygons(pp, col,\n",
170+
" f\"tolerance={tol} ({n_verts} verts, {reduction:.0f}% reduction)\",\n",
171+
" ax)\n",
172+
"\n",
173+
"plt.suptitle(\"Visvalingam-Whyatt simplification\", fontsize=14, y=1.02)\n",
174+
"plt.tight_layout()\n",
175+
"plt.show()"
176+
]
177+
},
178+
{
179+
"cell_type": "markdown",
180+
"metadata": {},
181+
"source": [
182+
"## GeoDataFrame output\n",
183+
"\n",
184+
"`simplify_tolerance` works with all return types including GeoDataFrame."
185+
]
186+
},
187+
{
188+
"cell_type": "code",
189+
"execution_count": null,
190+
"metadata": {},
191+
"outputs": [],
192+
"source": [
193+
"gdf = polygonize(raster, simplify_tolerance=1.5, return_type=\"geopandas\",\n",
194+
" column_name=\"landcover\")\n",
195+
"print(gdf.head(10))\n",
196+
"print(f\"\\nAll geometries valid: {gdf.geometry.is_valid.all()}\")"
197+
]
198+
}
199+
],
200+
"metadata": {
201+
"kernelspec": {
202+
"display_name": "Python 3",
203+
"language": "python",
204+
"name": "python3"
205+
},
206+
"language_info": {
207+
"name": "python",
208+
"version": "3.11.0"
209+
}
210+
},
211+
"nbformat": 4,
212+
"nbformat_minor": 4
213+
}

0 commit comments

Comments
 (0)