smoothify 0.2.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- smoothify-0.3.0/.github/workflows/ci.yml +40 -0
- smoothify-0.3.0/.github/workflows/publish.yml +75 -0
- smoothify-0.3.0/.gitignore +189 -0
- smoothify-0.3.0/.pre-commit-config.yaml +21 -0
- smoothify-0.3.0/.python-version +1 -0
- smoothify-0.3.0/.vscode/settings.json +43 -0
- smoothify-0.3.0/CHANGELOG.md +61 -0
- {smoothify-0.2.2 → smoothify-0.3.0}/PKG-INFO +79 -16
- smoothify-0.2.2/smoothify.egg-info/PKG-INFO → smoothify-0.3.0/README.md +63 -33
- smoothify-0.3.0/RELEASING.md +50 -0
- smoothify-0.3.0/benchmarks/bench_water.py +93 -0
- smoothify-0.3.0/examples/Water.gpkg +0 -0
- smoothify-0.3.0/examples/Water_Smoothed.gpkg +0 -0
- smoothify-0.3.0/examples/merge_holes_examples.ipynb +388 -0
- smoothify-0.3.0/examples/real_world_water_example.ipynb +773 -0
- smoothify-0.3.0/examples/smoothify_vs_shapely_comparison.ipynb +683 -0
- smoothify-0.3.0/examples/usage_examples.ipynb +837 -0
- smoothify-0.3.0/images/example_1_polygon.png +0 -0
- smoothify-0.3.0/images/example_2_linestring.png +0 -0
- smoothify-0.3.0/images/example_3_iterations.png +0 -0
- smoothify-0.3.0/images/example_4_merging.png +0 -0
- smoothify-0.3.0/images/generate_example_images.ipynb +366 -0
- smoothify-0.3.0/images/generate_pipeline_graphic.py +168 -0
- smoothify-0.3.0/images/generate_readme_image.ipynb +220 -0
- smoothify-0.3.0/images/pipeline_steps.png +0 -0
- smoothify-0.3.0/images/smoothify_hero.png +0 -0
- smoothify-0.3.0/images/smoothify_logo.png +0 -0
- smoothify-0.3.0/pyproject.toml +80 -0
- smoothify-0.3.0/pytest.ini +16 -0
- {smoothify-0.2.2 → smoothify-0.3.0}/smoothify/__init__.py +7 -1
- smoothify-0.3.0/smoothify/_version.py +24 -0
- {smoothify-0.2.2 → smoothify-0.3.0}/smoothify/coordinator.py +34 -7
- {smoothify-0.2.2 → smoothify-0.3.0}/smoothify/geometry_ops.py +259 -38
- smoothify-0.3.0/smoothify/py.typed +0 -0
- {smoothify-0.2.2 → smoothify-0.3.0}/smoothify/smoothify_core.py +165 -35
- smoothify-0.2.2/README.md → smoothify-0.3.0/smoothify.egg-info/PKG-INFO +96 -15
- smoothify-0.3.0/smoothify.egg-info/SOURCES.txt +62 -0
- smoothify-0.3.0/tests/README.md +26 -0
- smoothify-0.3.0/tests/__init__.py +1 -0
- smoothify-0.3.0/tests/conftest.py +42 -0
- {smoothify-0.2.2 → smoothify-0.3.0}/tests/test_all_geometry_types.py +187 -93
- {smoothify-0.2.2 → smoothify-0.3.0}/tests/test_area_tolerance.py +0 -1
- {smoothify-0.2.2 → smoothify-0.3.0}/tests/test_auto_segment_length.py +122 -55
- smoothify-0.3.0/tests/test_congruence_dedup.py +68 -0
- smoothify-0.3.0/tests/test_convexity_artifacts.py +66 -0
- smoothify-0.3.0/tests/test_corner_rounding.py +75 -0
- smoothify-0.3.0/tests/test_data/convex_pixel_rectangle.gpkg +0 -0
- smoothify-0.3.0/tests/test_data/naip_owm_water_bodies.geojson +121 -0
- {smoothify-0.2.2 → smoothify-0.3.0}/tests/test_edge_cases_coverage.py +12 -5
- {smoothify-0.2.2 → smoothify-0.3.0}/tests/test_geometry_types.py +38 -24
- smoothify-0.3.0/tests/test_invalid_polygon.py +174 -0
- smoothify-0.3.0/tests/test_merge_holes.py +230 -0
- {smoothify-0.2.2 → smoothify-0.3.0}/tests/test_real_world_data.py +5 -1
- {smoothify-0.2.2 → smoothify-0.3.0}/tests/test_smoothify_api.py +40 -33
- {smoothify-0.2.2 → smoothify-0.3.0}/tests/test_smoothify_core.py +19 -8
- smoothify-0.3.0/tests/test_synthetic_blob_fuzz.py +51 -0
- smoothify-0.3.0/tests/test_water_quality_sweep.py +103 -0
- smoothify-0.3.0/tests/visual_tests.ipynb +1231 -0
- smoothify-0.2.2/pyproject.toml +0 -43
- smoothify-0.2.2/smoothify/__version__.py +0 -1
- smoothify-0.2.2/smoothify.egg-info/SOURCES.txt +0 -22
- {smoothify-0.2.2 → smoothify-0.3.0}/LICENSE +0 -0
- {smoothify-0.2.2 → smoothify-0.3.0}/setup.cfg +0 -0
- {smoothify-0.2.2 → smoothify-0.3.0}/smoothify.egg-info/dependency_links.txt +0 -0
- {smoothify-0.2.2 → smoothify-0.3.0}/smoothify.egg-info/requires.txt +0 -0
- {smoothify-0.2.2 → smoothify-0.3.0}/smoothify.egg-info/top_level.txt +0 -0
- {smoothify-0.2.2 → smoothify-0.3.0}/tests/test_chaikin.py +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
check:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v6
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0 # full history so setuptools-scm can read tags
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v6
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
24
|
+
with:
|
|
25
|
+
cache-suffix: ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- run: uv sync --all-extras --dev
|
|
28
|
+
|
|
29
|
+
- name: Lint
|
|
30
|
+
run: uv run ruff check .
|
|
31
|
+
|
|
32
|
+
- name: Type check
|
|
33
|
+
run: uv run mypy smoothify/
|
|
34
|
+
|
|
35
|
+
- name: Test
|
|
36
|
+
run: uv run pytest tests/ -x -q
|
|
37
|
+
|
|
38
|
+
- name: Notebook smoke test
|
|
39
|
+
if: matrix.python-version == '3.11'
|
|
40
|
+
run: uv run pytest --nbmake examples/*.ipynb -q
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Triggered by pushing a tag that looks like a version, e.g. `v1.0.0`.
|
|
4
|
+
# The version comes from the git tag itself (via setuptools-scm) — no file to bump.
|
|
5
|
+
# Publishing uses PyPI trusted publishing (OIDC) — no API tokens, no secrets.
|
|
6
|
+
# To enable: at https://pypi.org/manage/project/smoothify/settings/publishing/
|
|
7
|
+
# add a publisher with:
|
|
8
|
+
# Owner DPIRD-DMA
|
|
9
|
+
# Repository name Smoothify
|
|
10
|
+
# Workflow name publish.yml
|
|
11
|
+
# Environment pypi
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
push:
|
|
15
|
+
tags:
|
|
16
|
+
- "v[0-9]+.[0-9]+.[0-9]+*"
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
build:
|
|
20
|
+
name: Build distribution
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v6
|
|
24
|
+
with:
|
|
25
|
+
fetch-depth: 0 # full history so setuptools-scm can read tags
|
|
26
|
+
|
|
27
|
+
- uses: actions/setup-python@v6
|
|
28
|
+
with:
|
|
29
|
+
python-version: "3.11"
|
|
30
|
+
|
|
31
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
32
|
+
|
|
33
|
+
- name: Build sdist + wheel
|
|
34
|
+
run: uv build
|
|
35
|
+
|
|
36
|
+
- uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: python-package-distributions
|
|
39
|
+
path: dist/
|
|
40
|
+
|
|
41
|
+
publish-to-pypi:
|
|
42
|
+
name: Publish to PyPI
|
|
43
|
+
needs: build
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
environment:
|
|
46
|
+
name: pypi
|
|
47
|
+
url: https://pypi.org/p/smoothify
|
|
48
|
+
permissions:
|
|
49
|
+
id-token: write # required for trusted publishing
|
|
50
|
+
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/download-artifact@v4
|
|
53
|
+
with:
|
|
54
|
+
name: python-package-distributions
|
|
55
|
+
path: dist/
|
|
56
|
+
|
|
57
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
58
|
+
|
|
59
|
+
github-release:
|
|
60
|
+
name: Create GitHub Release
|
|
61
|
+
needs: publish-to-pypi
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
permissions:
|
|
64
|
+
contents: write # required to create releases
|
|
65
|
+
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/download-artifact@v4
|
|
68
|
+
with:
|
|
69
|
+
name: python-package-distributions
|
|
70
|
+
path: dist/
|
|
71
|
+
|
|
72
|
+
- uses: softprops/action-gh-release@v2
|
|
73
|
+
with:
|
|
74
|
+
files: dist/*
|
|
75
|
+
generate_release_notes: true
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
#pdm.lock
|
|
113
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
114
|
+
# in version control.
|
|
115
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
116
|
+
.pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
121
|
+
__pypackages__/
|
|
122
|
+
|
|
123
|
+
# Celery stuff
|
|
124
|
+
celerybeat-schedule
|
|
125
|
+
celerybeat.pid
|
|
126
|
+
|
|
127
|
+
# SageMath parsed files
|
|
128
|
+
*.sage.py
|
|
129
|
+
|
|
130
|
+
# Environments
|
|
131
|
+
.env
|
|
132
|
+
.venv
|
|
133
|
+
env/
|
|
134
|
+
venv/
|
|
135
|
+
ENV/
|
|
136
|
+
env.bak/
|
|
137
|
+
venv.bak/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# PyCharm
|
|
164
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
165
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
166
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
167
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
168
|
+
#.idea/
|
|
169
|
+
|
|
170
|
+
# Ruff stuff:
|
|
171
|
+
.ruff_cache/
|
|
172
|
+
|
|
173
|
+
# PyPI configuration file
|
|
174
|
+
.pypirc
|
|
175
|
+
private test data/*
|
|
176
|
+
*.gpkg-shm
|
|
177
|
+
*.gpkg-wal
|
|
178
|
+
*.DS_Store
|
|
179
|
+
test data/output/*
|
|
180
|
+
examples/Water_Smoothed.gpkg
|
|
181
|
+
|
|
182
|
+
# setuptools-scm generated version file
|
|
183
|
+
smoothify/_version.py
|
|
184
|
+
|
|
185
|
+
# uv lockfile (not tracked — library, resolve fresh)
|
|
186
|
+
uv.lock
|
|
187
|
+
|
|
188
|
+
# Benchmarks
|
|
189
|
+
benchmarks/baseline_output.gpkg
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.15.8
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff-check
|
|
6
|
+
- id: ruff-format
|
|
7
|
+
- repo: local
|
|
8
|
+
hooks:
|
|
9
|
+
- id: mypy
|
|
10
|
+
name: mypy
|
|
11
|
+
entry: uv run mypy smoothify/
|
|
12
|
+
language: system
|
|
13
|
+
pass_filenames: false
|
|
14
|
+
always_run: true
|
|
15
|
+
- id: pytest
|
|
16
|
+
name: pytest
|
|
17
|
+
entry: uv run pytest tests/ -x -q -m "not slow"
|
|
18
|
+
language: system
|
|
19
|
+
pass_filenames: false
|
|
20
|
+
always_run: true
|
|
21
|
+
stages: [pre-push]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cSpell.words": [
|
|
3
|
+
"brentq",
|
|
4
|
+
"capstyle",
|
|
5
|
+
"chaikin",
|
|
6
|
+
"darkgreen",
|
|
7
|
+
"edgecolor",
|
|
8
|
+
"facecolor",
|
|
9
|
+
"figsize",
|
|
10
|
+
"fontsize",
|
|
11
|
+
"fontweight",
|
|
12
|
+
"geodataframe",
|
|
13
|
+
"geopandas",
|
|
14
|
+
"gpkg",
|
|
15
|
+
"hausdorffs",
|
|
16
|
+
"ipykernel",
|
|
17
|
+
"joblib",
|
|
18
|
+
"lightgreen",
|
|
19
|
+
"linalg",
|
|
20
|
+
"linearing",
|
|
21
|
+
"linestrings",
|
|
22
|
+
"matplotlib",
|
|
23
|
+
"multipolygons",
|
|
24
|
+
"ndarray",
|
|
25
|
+
"numpy",
|
|
26
|
+
"polygonized",
|
|
27
|
+
"pytest",
|
|
28
|
+
"scipy",
|
|
29
|
+
"segmentize",
|
|
30
|
+
"setuptools",
|
|
31
|
+
"shapley",
|
|
32
|
+
"smoothify",
|
|
33
|
+
"soomthify",
|
|
34
|
+
"suptitle",
|
|
35
|
+
"xlabel",
|
|
36
|
+
"xlim",
|
|
37
|
+
"xticks",
|
|
38
|
+
"xtol",
|
|
39
|
+
"ylabel",
|
|
40
|
+
"ylim",
|
|
41
|
+
"yticks"
|
|
42
|
+
],
|
|
43
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.3.0] - 2026-06-12
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Single-core smoothing is ~3.5x faster on typical raster-derived data (benchmarked on `examples/Water.gpkg`: 6.8s → 1.9s, including the cost of the new fold-repair check below). Output differences are sub-pixel (bounded by the algorithm's own start-point noise floor); area preservation accuracy is unchanged or slightly better. The main changes:
|
|
11
|
+
- Removed the reversed-direction smoothing variants for polygons: Chaikin corner cutting is direction-invariant on closed rings, so they duplicated the forward variants bit-for-bit and only inflated the variant union (output unchanged).
|
|
12
|
+
- Holes are now subtracted in a single `difference` call against their union instead of one `intersection` + `difference` pair per hole (output unchanged).
|
|
13
|
+
- The tiny merge/dissolve buffer now uses mitre joins, which keep corners as single vertices instead of adding ~8 arc vertices per corner (boundary differences at the millimetre scale of the buffer itself).
|
|
14
|
+
- Pre-union Chaikin smoothing of the start-point variants is capped at 2 iterations; detail beyond that was erased by the post-union simplify anyway, while doubling the vertex count entering the expensive union. The final smoothing pass still runs the full `smooth_iterations`.
|
|
15
|
+
- The area-preservation root finder brackets the root from one side using its linear estimate, caches evaluations, and uses a step tolerance derived from the area tolerance via the perimeter, roughly halving the number of buffer operations.
|
|
16
|
+
- Congruent geometries (translated copies of the same shape, common in raster-derived data) are now smoothed once and the result translated to each occurrence; this also reduces work dispatched to parallel workers.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- New `merge_holes` option (default `True`): holes that touch or nearly touch (e.g. diagonally adjacent raster cells) are joined before smoothing, so they smooth into one coherent opening instead of separate overlapping shapes leaving a fake land bridge. Pass `merge_holes=False` for the previous per-hole behaviour. Mirrors what `merge_multipolygons` does for shells.
|
|
20
|
+
- `examples/merge_holes_examples.ipynb`: worked examples of `merge_holes` and its interplay with `merge_collection` (touching holes, overlapping donuts, holes split across features).
|
|
21
|
+
- `benchmarks/bench_water.py`: single-core timing/profiling benchmark with baseline output comparison.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Fixed sharp concave folds in smoothed output on shapes with features about one `segment_length` wide (e.g. a hole with a one-pixel-wide arm, as produced by `merge_holes` joining a small hole to a larger one). The start-point smoothing variants can disagree about such features, leaving a forked slit in their union that the area-preservation shrink sharpens into a cusp. The final result is now checked for sharp concave turns and, when one is found, recomputed from the variant union sealed with a small closing (dilate-erode at `segment_length / 4`).
|
|
25
|
+
- Fixed sharp cusps where a smoothed hole crosses the independently smoothed exterior and gets clipped by the hole subtraction (previously up to a 180-degree fold at the tangential crossing points). When detected, the result is repaired with a small opening + closing (at `segment_length / 4`), which removes hair-thin material needles and seals thin slits without visibly moving the boundary.
|
|
26
|
+
- Fixed shapes with long straight edges being massively over-rounded (e.g. a large square with a small `segment_length` collapsed into a circle). The simplify steps strip all collinear vertices from straight edges, and Chaikin's corner cuts scale with segment length, so corner rounding grew with edge length instead of `segment_length`. Geometries are now re-segmentized after each simplify step, capping corner rounding at roughly `segment_length` while still smoothing raster staircase artifacts into curves. Applies to all geometry types. Note: outputs are somewhat denser (more vertices) and smoothing is ~20% slower on typical raster-derived data.
|
|
27
|
+
|
|
28
|
+
## [0.2.3] - 2026-06-02
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- The package now ships inline type hints and a `py.typed` marker, so type checkers (mypy, Pyright/Pylance) pick up `smoothify()`'s signatures and overloads in downstream code.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- Invalid geometries (e.g. self-intersecting polygons) are now returned unchanged with a warning instead of crashing with a cryptic error or silently collapsing to an empty geometry. Behaviour is consistent across single geometries, lists/collections, and GeoDataFrames (and regardless of `merge_collection`). Repair them with shapely's `make_valid()` first if you want them smoothed.
|
|
35
|
+
|
|
36
|
+
## [0.2.2] - 2026-03-24
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- Fixed LineString smoothing: self-intersecting lines are no longer run through the polygon-only `make_valid`/`unary_union` step, which could split them into a MultiLineString at crossing points
|
|
40
|
+
- Preserve coordinate dimensionality during Chaikin corner cutting instead of forcing 2D output, so smoothed geometries keep their original dimensions
|
|
41
|
+
|
|
42
|
+
## [0.2.1] - 2026-02-25
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- Fixed `TopologyException` crash on thin/elongated polygons by validating smoothed variants before union
|
|
46
|
+
- Fixed crash when hole subtraction splits a polygon into a MultiPolygon (e.g. tiny holes relative to segment length)
|
|
47
|
+
|
|
48
|
+
## [0.2.0] - 2026-02-25
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- `smooth_iterations=0` now returns the original input unchanged instead of running the geometry through segmentize/simplify pipeline without smoothing
|
|
52
|
+
|
|
53
|
+
## [0.1.0] - 2025-11-25
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
- Initial public release
|
|
57
|
+
- Core smoothing functionality using Chaikin's corner-cutting algorithm
|
|
58
|
+
- Support for all Shapely geometry types (Polygon, LineString, MultiPolygon, etc.)
|
|
59
|
+
- Automatic segment length detection
|
|
60
|
+
- Parallel processing support
|
|
61
|
+
- Area preservation for polygons
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smoothify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Transform pixelated geometries from raster data into smooth natural looking features
|
|
5
5
|
Author-email: Nick Wright <nicholas.wright@dpird.wa.gov.au>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/DPIRD-DMA/Smoothify
|
|
8
|
+
Project-URL: Repository, https://github.com/DPIRD-DMA/Smoothify
|
|
9
|
+
Project-URL: Issues, https://github.com/DPIRD-DMA/Smoothify/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/DPIRD-DMA/Smoothify/blob/main/CHANGELOG.md
|
|
8
11
|
Keywords: geometry,smoothing,smooth,GIS,raster,vector,chaikin,shapely,geopandas
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
9
24
|
Requires-Python: >=3.10
|
|
10
25
|
Description-Content-Type: text/markdown
|
|
11
26
|
License-File: LICENSE
|
|
@@ -16,15 +31,18 @@ Requires-Dist: scipy>=1.11.0
|
|
|
16
31
|
Requires-Dist: shapely>=2.0.2
|
|
17
32
|
Dynamic: license-file
|
|
18
33
|
|
|
19
|
-
<p align="
|
|
34
|
+
<p align="center">
|
|
20
35
|
<img src="https://raw.githubusercontent.com/DPIRD-DMA/Smoothify/main/images/smoothify_logo.png" alt="Smoothify Text" width="600">
|
|
21
36
|
</p>
|
|
22
37
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
<p align="center">
|
|
39
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%2B-blue" alt="Python Version"></a>
|
|
40
|
+
<a href="https://github.com/DPIRD-DMA/Smoothify/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
|
|
41
|
+
<a href="https://pypi.org/project/smoothify/"><img src="https://img.shields.io/pypi/v/smoothify.svg" alt="PyPI version"></a>
|
|
42
|
+
<a href="https://pepy.tech/projects/smoothify"><img src="https://static.pepy.tech/badge/smoothify" alt="PyPI downloads"></a>
|
|
43
|
+
<a href="https://anaconda.org/conda-forge/smoothify"><img src="https://img.shields.io/conda/vn/conda-forge/smoothify.svg" alt="Conda version"></a>
|
|
44
|
+
<a href="https://github.com/DPIRD-DMA/Smoothify/tree/main/examples"><img src="https://img.shields.io/badge/Tutorials-Learn-brightgreen" alt="Tutorials"></a>
|
|
45
|
+
</p>
|
|
28
46
|
|
|
29
47
|
📋 [View Changelog](https://github.com/DPIRD-DMA/Smoothify/blob/main/CHANGELOG.md)
|
|
30
48
|
|
|
@@ -43,7 +61,7 @@ Polygons and lines derived from classified raster data (e.g., ML model predictio
|
|
|
43
61
|
Smoothify applies an optimized implementation of Chaikin's corner-cutting algorithm along with other geometric processing to create smooth, natural-looking features while:
|
|
44
62
|
|
|
45
63
|
- Preserving the general shape and area of polygons
|
|
46
|
-
- Supporting all
|
|
64
|
+
- Supporting all shapely geometry types
|
|
47
65
|
- Handling shapes with interior holes
|
|
48
66
|
- Efficiently processing large datasets with multiprocessing
|
|
49
67
|
|
|
@@ -100,6 +118,7 @@ Example notebooks:
|
|
|
100
118
|
- [Usage examples](https://github.com/DPIRD-DMA/Smoothify/blob/main/examples/usage_examples.ipynb)
|
|
101
119
|
- [Smoothify vs. Shapely comparison](https://github.com/DPIRD-DMA/Smoothify/blob/main/examples/smoothify_vs_shapely_comparison.ipynb)
|
|
102
120
|
- [Real-world water example](https://github.com/DPIRD-DMA/Smoothify/blob/main/examples/real_world_water_example.ipynb)
|
|
121
|
+
- [Merging holes](https://github.com/DPIRD-DMA/Smoothify/blob/main/examples/merge_holes_examples.ipynb)
|
|
103
122
|
|
|
104
123
|
|
|
105
124
|
### Basic Polygon Smoothing
|
|
@@ -204,6 +223,7 @@ smoothed = smoothify(
|
|
|
204
223
|
| `merge_collection` | bool | True | Whether to merge/dissolve adjacent geometries in collections before smoothing |
|
|
205
224
|
| `merge_field` | str | None | **GeoDataFrame only**: Column name to use for dissolving geometries. Only valid when `merge_collection=True`. If None, dissolves all geometries together. If specified, dissolves geometries grouped by the column values |
|
|
206
225
|
| `merge_multipolygons` | bool | True | Whether to merge adjacent polygons within MultiPolygons before smoothing |
|
|
226
|
+
| `merge_holes` | bool | True | Whether to join holes that touch or nearly touch (e.g. diagonally adjacent raster cells) before smoothing, so they smooth into one coherent opening instead of separate overlapping shapes |
|
|
207
227
|
| `preserve_area` | bool | True | Whether to restore original area after smoothing via buffering (applies to Polygons only) |
|
|
208
228
|
| `area_tolerance` | float | 0.01 | Percentage of original area allowed as error (e.g., 0.01 = 0.01% error = 99.99% preservation). Only affects Polygons when preserve_area=True |
|
|
209
229
|
|
|
@@ -211,21 +231,64 @@ smoothed = smoothify(
|
|
|
211
231
|
|
|
212
232
|
Smoothify uses an advanced multi-step smoothing pipeline:
|
|
213
233
|
|
|
234
|
+
<p align="left">
|
|
235
|
+
<img src="https://raw.githubusercontent.com/DPIRD-DMA/Smoothify/main/images/pipeline_steps.png" alt="Smoothify pipeline steps" width="800">
|
|
236
|
+
</p>
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
1. Joins touching holes (for Polygons, when `merge_holes=True`) so they smooth as one opening
|
|
240
|
+
2. Adds intermediate vertices along line segments (segmentize)
|
|
241
|
+
3. Generates multiple rotated variants (for Polygons) to avoid artifacts
|
|
242
|
+
4. Simplifies each variant to remove noise
|
|
243
|
+
5. Applies Chaikin corner cutting to smooth
|
|
244
|
+
6. Merges all variants via union to eliminate start-point artifacts
|
|
245
|
+
7. Applies final smoothing pass
|
|
246
|
+
8. Optionally restores original area via buffering (for Polygons)
|
|
247
|
+
9. Detects and repairs any sharp folds left by features near the smoothing scale (e.g. one-pixel-wide arms), using a small morphological opening/closing bounded at `segment_length / 4`
|
|
214
248
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
249
|
+
## Invalid Geometries
|
|
250
|
+
|
|
251
|
+
Smoothify does not repair invalid input. If it encounters an invalid geometry (e.g. a self-intersecting polygon), it returns that geometry **unchanged** and emits a warning, instead of crashing or silently producing an empty geometry. This is consistent whether you pass a single geometry, a list/collection, or a GeoDataFrame.
|
|
252
|
+
|
|
253
|
+
If you want invalid geometries smoothed, repair them first with shapely's `make_valid()`:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
# GeoDataFrame
|
|
257
|
+
gdf.geometry = gdf.geometry.make_valid()
|
|
258
|
+
smoothed_gdf = smoothify(gdf, segment_length=10.0)
|
|
259
|
+
|
|
260
|
+
# Single geometry
|
|
261
|
+
from shapely import make_valid
|
|
262
|
+
smoothed = smoothify(make_valid(polygon), segment_length=1.0)
|
|
263
|
+
```
|
|
222
264
|
|
|
223
265
|
## Performance Considerations
|
|
224
266
|
|
|
225
267
|
- **Parallel Processing**: For large GeoDataFrames or collections, use `num_cores` = 0 to enable parallel processing
|
|
268
|
+
- **Duplicate Shapes**: Geometries that are translated copies of the same shape (common in raster-derived data, e.g. single-pixel polygons) are automatically smoothed once and the result reused
|
|
226
269
|
- **Smoothing Iterations**: Values of 3-5 typically provide good results. Higher values create smoother output but increase processing time and vertex count
|
|
227
270
|
- **Memory Usage**: Scales with geometry complexity. The algorithm creates multiple variants during smoothing
|
|
228
|
-
- **Optimal segment_length**:
|
|
271
|
+
- **Optimal segment_length**: Anything from about half the original raster pixel size and up should produce reasonable output — larger values produce more rounded output, smaller values stay more faithful to the original geometry
|
|
272
|
+
|
|
273
|
+
## Running the Tests
|
|
274
|
+
|
|
275
|
+
Smoothify uses [pytest](https://pytest.org/). After cloning the repository, install the development dependencies and run the suite with [uv](https://docs.astral.sh/uv/):
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
# Install dependencies (including the dev group)
|
|
279
|
+
uv sync
|
|
280
|
+
|
|
281
|
+
# Run all tests
|
|
282
|
+
uv run pytest tests/
|
|
283
|
+
|
|
284
|
+
# Run with coverage
|
|
285
|
+
uv run pytest tests/ --cov=smoothify --cov-report=html
|
|
286
|
+
|
|
287
|
+
# Run a single test
|
|
288
|
+
uv run pytest tests/test_chaikin.py::TestChaikinCornerCutting::test_simple_square_polygon
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
If you prefer not to use uv, install the dev dependencies into your environment and run `pytest tests/` directly.
|
|
229
292
|
|
|
230
293
|
## Contributing
|
|
231
294
|
|