Skip to content

Commit 11e25f8

Browse files
authored
Add support for building regulariser (#142)
* Add support for building regulariser * Deprecate Python 3.9 * Change to optional dependency * Update import * Update pyproject.toml * Add dependency * Allow notebook execute
1 parent a95c471 commit 11e25f8

File tree

8 files changed

+297
-11
lines changed

8 files changed

+297
-11
lines changed

.github/workflows/docs-build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ jobs:
3535
run: |
3636
uv pip install --find-links https://girder.github.io/large_image_wheels GDAL pyproj
3737
uv pip install pytest
38+
uv pip install "geoai-py[extra]"
3839
3940
- name: Test import
4041
run: |

.github/workflows/docs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ jobs:
3434
run: |
3535
uv pip install --find-links https://girder.github.io/large_image_wheels GDAL pyproj
3636
uv pip install pytest
37+
uv pip install "geoai-py[extra]"
3738
3839
- name: Test import
3940
run: |

.github/workflows/ubuntu.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
strategy:
1515
fail-fast: false
1616
matrix:
17-
python-version: ["3.9", "3.10", "3.11", "3.12"]
17+
python-version: ["3.10", "3.11", "3.12"]
1818

1919
steps:
2020
- uses: actions/checkout@v4
@@ -37,6 +37,7 @@ jobs:
3737
run: |
3838
uv pip install --find-links https://girder.github.io/large_image_wheels gdal pyproj
3939
uv pip install pytest
40+
uv pip install "geoai-py[extra]"
4041
4142
- name: Test import
4243
run: |

docs/examples/regularization.ipynb

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Regularization\n",
8+
"\n",
9+
"[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/geoai/blob/main/docs/examples/regularization.ipynb)\n",
10+
"\n",
11+
"This example demonstrates how to regularize building footprints using the `regularize` function from the `geoai` package. The function is a wrapper around the `regularize_geodataframe` function from the [buildingregulariser](https://github.com/DPIRD-DMA/Building-Regulariser) package. Credits to the original author, Nick Wright.\n",
12+
"\n",
13+
"It can be used to regularize features such as building footprints, solar panels, cars, and other objects that have a regular shape.\n",
14+
"\n",
15+
"## Install package\n",
16+
"To use the `geoai-py` package, ensure it is installed in your environment. Uncomment the command below if needed."
17+
]
18+
},
19+
{
20+
"cell_type": "code",
21+
"execution_count": null,
22+
"metadata": {},
23+
"outputs": [],
24+
"source": [
25+
"# %pip install geoai-py"
26+
]
27+
},
28+
{
29+
"cell_type": "markdown",
30+
"metadata": {},
31+
"source": [
32+
"## Import libraries"
33+
]
34+
},
35+
{
36+
"cell_type": "code",
37+
"execution_count": null,
38+
"metadata": {},
39+
"outputs": [],
40+
"source": [
41+
"import geoai"
42+
]
43+
},
44+
{
45+
"cell_type": "markdown",
46+
"metadata": {},
47+
"source": [
48+
"## Use sample data"
49+
]
50+
},
51+
{
52+
"cell_type": "code",
53+
"execution_count": null,
54+
"metadata": {},
55+
"outputs": [],
56+
"source": [
57+
"raster_url = (\n",
58+
" \"https://huggingface.co/datasets/giswqs/geospatial/resolve/main/naip_train.tif\"\n",
59+
")\n",
60+
"vector_url = \"https://huggingface.co/datasets/giswqs/geospatial/resolve/main/buildings_original.geojson\""
61+
]
62+
},
63+
{
64+
"cell_type": "markdown",
65+
"metadata": {},
66+
"source": [
67+
"## Visualize data"
68+
]
69+
},
70+
{
71+
"cell_type": "code",
72+
"execution_count": null,
73+
"metadata": {},
74+
"outputs": [],
75+
"source": [
76+
"geoai.view_vector_interactive(vector_url, tiles=raster_url)"
77+
]
78+
},
79+
{
80+
"cell_type": "markdown",
81+
"metadata": {},
82+
"source": [
83+
"## Regularize buildings"
84+
]
85+
},
86+
{
87+
"cell_type": "code",
88+
"execution_count": null,
89+
"metadata": {},
90+
"outputs": [],
91+
"source": [
92+
"gdf = geoai.regularize(\n",
93+
" data=vector_url,\n",
94+
" simplify_tolerance=2.0,\n",
95+
" allow_45_degree=True,\n",
96+
" diagonal_threshold_reduction=30,\n",
97+
" allow_circles=True,\n",
98+
" circle_threshold=0.9,\n",
99+
")"
100+
]
101+
},
102+
{
103+
"cell_type": "code",
104+
"execution_count": null,
105+
"metadata": {},
106+
"outputs": [],
107+
"source": [
108+
"geoai.view_vector_interactive(gdf, tiles=raster_url)"
109+
]
110+
},
111+
{
112+
"cell_type": "code",
113+
"execution_count": null,
114+
"metadata": {},
115+
"outputs": [],
116+
"source": [
117+
"geoai.create_split_map(\n",
118+
" left_layer=gdf,\n",
119+
" right_layer=raster_url,\n",
120+
" left_label=\"Regularized Buildings\",\n",
121+
" right_label=\"NAIP Imagery\",\n",
122+
" left_args={\"style\": {\"color\": \"red\", \"fillOpacity\": 0.3}},\n",
123+
" basemap=raster_url,\n",
124+
")"
125+
]
126+
},
127+
{
128+
"cell_type": "markdown",
129+
"metadata": {},
130+
"source": [
131+
"## Compare results"
132+
]
133+
},
134+
{
135+
"cell_type": "code",
136+
"execution_count": null,
137+
"metadata": {},
138+
"outputs": [],
139+
"source": [
140+
"import leafmap.foliumap as leafmap"
141+
]
142+
},
143+
{
144+
"cell_type": "code",
145+
"execution_count": null,
146+
"metadata": {},
147+
"outputs": [],
148+
"source": [
149+
"m = leafmap.Map()\n",
150+
"m.add_cog_layer(raster_url, name=\"NAIP\")\n",
151+
"m.add_vector(\n",
152+
" vector_url, style={\"color\": \"yellow\", \"fillOpacity\": 0}, layer_name=\"Original\"\n",
153+
")\n",
154+
"m.add_gdf(gdf, style={\"color\": \"red\", \"fillOpacity\": 0}, layer_name=\"Regularized\")\n",
155+
"legend = {\n",
156+
" \"Original\": \"yellow\",\n",
157+
" \"Regularized\": \"red\",\n",
158+
"}\n",
159+
"m.add_legend(title=\"Building Footprints\", legend_dict=legend)\n",
160+
"m"
161+
]
162+
}
163+
],
164+
"metadata": {
165+
"kernelspec": {
166+
"display_name": "geo",
167+
"language": "python",
168+
"name": "python3"
169+
},
170+
"language_info": {
171+
"codemirror_mode": {
172+
"name": "ipython",
173+
"version": 3
174+
},
175+
"file_extension": ".py",
176+
"mimetype": "text/x-python",
177+
"name": "python",
178+
"nbconvert_exporter": "python",
179+
"pygments_lexer": "ipython3",
180+
"version": "3.12.9"
181+
}
182+
},
183+
"nbformat": 4,
184+
"nbformat_minor": 2
185+
}

geoai/utils.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6249,3 +6249,99 @@ def download_model_from_hf(model_path, repo_id=None):
62496249
print(f"Error downloading model from Hugging Face: {e}")
62506250
print("Please specify a local model path or ensure internet connectivity.")
62516251
raise
6252+
6253+
6254+
def regularize(
6255+
data: Union[gpd.GeoDataFrame, str],
6256+
parallel_threshold: float = 1.0,
6257+
target_crs: Optional[Union[str, "pyproj.CRS"]] = None,
6258+
simplify: bool = True,
6259+
simplify_tolerance: float = 0.5,
6260+
allow_45_degree: bool = True,
6261+
diagonal_threshold_reduction: float = 15,
6262+
allow_circles: bool = True,
6263+
circle_threshold: float = 0.9,
6264+
num_cores: int = 1,
6265+
include_metadata: bool = False,
6266+
output_path: Optional[str] = None,
6267+
**kwargs,
6268+
) -> gpd.GeoDataFrame:
6269+
"""Regularizes polygon geometries in a GeoDataFrame by aligning edges.
6270+
6271+
Aligns edges to be parallel or perpendicular (optionally also 45 degrees)
6272+
to their main direction. Handles reprojection, initial simplification,
6273+
regularization, geometry cleanup, and parallel processing.
6274+
6275+
This function is a wrapper around the `regularize_geodataframe` function
6276+
from the `buildingregulariser` package. Credits to the original author
6277+
Nick Wright. Check out the repo at https://github.com/DPIRD-DMA/Building-Regulariser.
6278+
6279+
Args:
6280+
data (Union[gpd.GeoDataFrame, str]): Input GeoDataFrame with polygon or multipolygon geometries,
6281+
or a file path to the GeoDataFrame.
6282+
parallel_threshold (float, optional): Distance threshold for merging nearly parallel adjacent edges
6283+
during regularization. Defaults to 1.0.
6284+
target_crs (Optional[Union[str, "pyproj.CRS"]], optional): Target Coordinate Reference System for
6285+
processing. If None, uses the input GeoDataFrame's CRS. Processing is more reliable in a
6286+
projected CRS. Defaults to None.
6287+
simplify (bool, optional): If True, applies initial simplification to the geometry before
6288+
regularization. Defaults to True.
6289+
simplify_tolerance (float, optional): Tolerance for the initial simplification step (if `simplify`
6290+
is True). Also used for geometry cleanup steps. Defaults to 0.5.
6291+
allow_45_degree (bool, optional): If True, allows edges to be oriented at 45-degree angles relative
6292+
to the main direction during regularization. Defaults to True.
6293+
diagonal_threshold_reduction (float, optional): Reduction factor in degrees to reduce the likelihood
6294+
of diagonal edges being created. Larger values reduce the likelihood of diagonal edges.
6295+
Defaults to 15.
6296+
allow_circles (bool, optional): If True, attempts to detect polygons that are nearly circular and
6297+
replaces them with perfect circles. Defaults to True.
6298+
circle_threshold (float, optional): Intersection over Union (IoU) threshold used for circle detection
6299+
(if `allow_circles` is True). Value between 0 and 1. Defaults to 0.9.
6300+
num_cores (int, optional): Number of CPU cores to use for parallel processing. If 1, processing is
6301+
done sequentially. Defaults to 1.
6302+
include_metadata (bool, optional): If True, includes metadata about the regularization process in the
6303+
output GeoDataFrame. Defaults to False.
6304+
output_path (Optional[str], optional): Path to save the output GeoDataFrame. If None, the output is
6305+
not saved. Defaults to None.
6306+
**kwargs: Additional keyword arguments to pass to the `to_file` method when saving the output.
6307+
6308+
Returns:
6309+
gpd.GeoDataFrame: A new GeoDataFrame with regularized polygon geometries. Original attributes are
6310+
preserved. Geometries that failed processing might be dropped.
6311+
6312+
Raises:
6313+
ValueError: If the input data is not a GeoDataFrame or a file path, or if the input GeoDataFrame is empty.
6314+
"""
6315+
try:
6316+
from buildingregulariser import regularize_geodataframe
6317+
except ImportError:
6318+
install_package("buildingregulariser")
6319+
from buildingregulariser import regularize_geodataframe
6320+
6321+
if isinstance(data, str):
6322+
data = gpd.read_file(data)
6323+
elif not isinstance(data, gpd.GeoDataFrame):
6324+
raise ValueError("Input data must be a GeoDataFrame or a file path.")
6325+
6326+
# Check if the input data is empty
6327+
if data.empty:
6328+
raise ValueError("Input GeoDataFrame is empty.")
6329+
6330+
gdf = regularize_geodataframe(
6331+
data,
6332+
parallel_threshold=parallel_threshold,
6333+
target_crs=target_crs,
6334+
simplify=simplify,
6335+
simplify_tolerance=simplify_tolerance,
6336+
allow_45_degree=allow_45_degree,
6337+
diagonal_threshold_reduction=diagonal_threshold_reduction,
6338+
allow_circles=allow_circles,
6339+
circle_threshold=circle_threshold,
6340+
num_cores=num_cores,
6341+
include_metadata=include_metadata,
6342+
)
6343+
6344+
if output_path:
6345+
gdf.to_file(output_path, **kwargs)
6346+
6347+
return gdf

mkdocs.yml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,21 @@ plugins:
4747
- mkdocs-jupyter:
4848
include_source: True
4949
ignore_h1_titles: True
50-
execute: false
50+
execute: true
5151
allow_errors: false
5252
ignore: ["conf.py"]
53-
execute_ignore: [
53+
execute_ignore:
54+
[
5455
"examples/dataviz/*.ipynb",
5556
"examples/rastervision/*.ipynb",
5657
"examples/samgeo/*.ipynb",
58+
"examples/download_*.ipynb",
59+
"examples/planetary_computer.ipynb",
60+
"examples/*_detection.ipynb",
61+
"examples/building_footprints_*.ipynb",
5762
"examples/data_visualization.ipynb",
58-
"examples/download_data.ipynb",
59-
"examples/car_detection.ipynb",
60-
"examples/ship_detection.ipynb",
61-
# "examples/solar_panel_detection.ipynb",
62-
"examples/building_footprints_africa.ipynb",
63-
"examples/building_footprints_china.ipynb",
63+
"examples/train_*.ipynb",
64+
"examples/wetland_mapping.ipynb",
6465
]
6566
# - mkdocstrings:
6667
# default_handler: python
@@ -124,6 +125,7 @@ nav:
124125
- examples/train_ship_detection.ipynb
125126
- examples/train_water_detection.ipynb
126127
- examples/wetland_mapping.ipynb
128+
- examples/regularization.ipynb
127129
# - examples/samgeo/satellite.ipynb
128130
# - examples/samgeo/automatic_mask_generator.ipynb
129131
# - examples/samgeo/automatic_mask_generator_hq.ipynb

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ dynamic = [
66
]
77
description = "A Python package for using Artificial Intelligence (AI) with geospatial data"
88
readme = "README.md"
9-
requires-python = ">=3.9"
9+
requires-python = ">=3.10"
1010
keywords = [
1111
"geoai",
1212
]
@@ -18,7 +18,6 @@ classifiers = [
1818
"Intended Audience :: Developers",
1919
"License :: OSI Approved :: MIT License",
2020
"Natural Language :: English",
21-
"Programming Language :: Python :: 3.9",
2221
"Programming Language :: Python :: 3.10",
2322
"Programming Language :: Python :: 3.11",
2423
"Programming Language :: Python :: 3.12",

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
albumentations
2+
buildingregulariser
23
contextily
34
geopandas
45
huggingface_hub

0 commit comments

Comments
 (0)