pysmrf 1.0.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.
- pysmrf-1.0.0/CITATION.cff +37 -0
- pysmrf-1.0.0/LICENSE +22 -0
- pysmrf-1.0.0/MANIFEST.in +9 -0
- pysmrf-1.0.0/PKG-INFO +269 -0
- pysmrf-1.0.0/README.md +218 -0
- pysmrf-1.0.0/pyproject.toml +73 -0
- pysmrf-1.0.0/setup.cfg +4 -0
- pysmrf-1.0.0/setup.py +61 -0
- pysmrf-1.0.0/src/pysmrf/__init__.py +96 -0
- pysmrf-1.0.0/src/pysmrf/cli.py +204 -0
- pysmrf-1.0.0/src/pysmrf/core.py +352 -0
- pysmrf-1.0.0/src/pysmrf/grid.py +210 -0
- pysmrf-1.0.0/src/pysmrf/inpaint.py +252 -0
- pysmrf-1.0.0/src/pysmrf/interp.py +188 -0
- pysmrf-1.0.0/src/pysmrf/io.py +307 -0
- pysmrf-1.0.0/src/pysmrf/morphology.py +176 -0
- pysmrf-1.0.0/src/pysmrf/parallel.py +60 -0
- pysmrf-1.0.0/src/pysmrf/types.py +199 -0
- pysmrf-1.0.0/src/pysmrf/visualization.py +95 -0
- pysmrf-1.0.0/src/pysmrf.egg-info/PKG-INFO +269 -0
- pysmrf-1.0.0/src/pysmrf.egg-info/SOURCES.txt +32 -0
- pysmrf-1.0.0/src/pysmrf.egg-info/dependency_links.txt +1 -0
- pysmrf-1.0.0/src/pysmrf.egg-info/entry_points.txt +2 -0
- pysmrf-1.0.0/src/pysmrf.egg-info/requires.txt +21 -0
- pysmrf-1.0.0/src/pysmrf.egg-info/top_level.txt +1 -0
- pysmrf-1.0.0/tests/run_test_on_laz.py +306 -0
- pysmrf-1.0.0/tests/test_backward_compat.py +51 -0
- pysmrf-1.0.0/tests/test_cli.py +44 -0
- pysmrf-1.0.0/tests/test_core.py +123 -0
- pysmrf-1.0.0/tests/test_grid.py +53 -0
- pysmrf-1.0.0/tests/test_inpaint.py +53 -0
- pysmrf-1.0.0/tests/test_interp.py +56 -0
- pysmrf-1.0.0/tests/test_io.py +43 -0
- pysmrf-1.0.0/tests/test_morphology.py +59 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software in your research, please cite both the foundational paper and this library."
|
|
3
|
+
title: "PySMRF: High-Performance Parallel Simple Morphological Filter for LiDAR Point Clouds"
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
date-released: 2026-09-11
|
|
6
|
+
license: MIT
|
|
7
|
+
repository-code: "https://github.com/thomaspingel/smrf"
|
|
8
|
+
identifiers:
|
|
9
|
+
- type: doi
|
|
10
|
+
value: 10.1016/j.isprsjprs.2012.12.002
|
|
11
|
+
description: "Foundational SMRF algorithm paper in ISPRS Journal of Photogrammetry and Remote Sensing"
|
|
12
|
+
authors:
|
|
13
|
+
- family-names: Pingel
|
|
14
|
+
given-names: Thomas J.
|
|
15
|
+
email: thomas.pingel@gmail.com
|
|
16
|
+
orcid: "https://orcid.org/0000-0002-3971-8573"
|
|
17
|
+
- family-names: Clarke
|
|
18
|
+
given-names: Keith C.
|
|
19
|
+
- family-names: McBride
|
|
20
|
+
given-names: William A.
|
|
21
|
+
preferred-citation:
|
|
22
|
+
type: article
|
|
23
|
+
title: "An improved simple morphological filter for producing ground surfaces from LiDAR point clouds"
|
|
24
|
+
authors:
|
|
25
|
+
- family-names: Pingel
|
|
26
|
+
given-names: Thomas J.
|
|
27
|
+
- family-names: Clarke
|
|
28
|
+
given-names: Keith C.
|
|
29
|
+
- family-names: McBride
|
|
30
|
+
given-names: William A.
|
|
31
|
+
journal: "ISPRS Journal of Photogrammetry and Remote Sensing"
|
|
32
|
+
volume: 77
|
|
33
|
+
start: 21
|
|
34
|
+
end: 30
|
|
35
|
+
year: 2013
|
|
36
|
+
doi: 10.1016/j.isprsjprs.2012.12.002
|
|
37
|
+
url: "https://doi.org/10.1016/j.isprsjprs.2012.12.002"
|
pysmrf-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013-2021 Thomas Pingel
|
|
4
|
+
Copyright (c) 2026 Aman Ranjan
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
pysmrf-1.0.0/MANIFEST.in
ADDED
pysmrf-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pysmrf
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: High-performance parallel Simple Morphological Filter (SMRF) for LiDAR ground classification and DEM generation citing Pingel et al. (2013)
|
|
5
|
+
Home-page: https://doi.org/10.1016/j.isprsjprs.2012.12.002
|
|
6
|
+
Author: Aman Ranjan, Thomas Pingel (original algorithm)
|
|
7
|
+
Author-email: Aman Ranjan <er.amanranjan@gmail.com>
|
|
8
|
+
Maintainer-email: Aman Ranjan <er.amanranjan@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Project-URL: Homepage, https://github.com/thomaspingel/smrf
|
|
11
|
+
Project-URL: Paper, https://doi.org/10.1016/j.isprsjprs.2012.12.002
|
|
12
|
+
Project-URL: Repository, https://github.com/thomaspingel/smrf
|
|
13
|
+
Keywords: lidar,point-cloud,gis,dem,dtm,morphological-filter,ground-classification,parallel,remote-sensing
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Operating System :: OS Independent
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: numpy>=1.22.0
|
|
30
|
+
Requires-Dist: scipy>=1.8.0
|
|
31
|
+
Requires-Dist: rasterio>=1.3.0
|
|
32
|
+
Requires-Dist: pandas>=1.4.0
|
|
33
|
+
Requires-Dist: click>=8.0.0
|
|
34
|
+
Requires-Dist: laspy[laszip,lazrs]>=2.6.1
|
|
35
|
+
Provides-Extra: viz
|
|
36
|
+
Requires-Dist: matplotlib>=3.5.0; extra == "viz"
|
|
37
|
+
Provides-Extra: all
|
|
38
|
+
Requires-Dist: lazrs>=0.5.0; extra == "all"
|
|
39
|
+
Requires-Dist: matplotlib>=3.5.0; extra == "all"
|
|
40
|
+
Requires-Dist: scikit-image>=0.19.0; extra == "all"
|
|
41
|
+
Provides-Extra: dev
|
|
42
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
44
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
45
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
46
|
+
Requires-Dist: lazrs>=0.5.0; extra == "dev"
|
|
47
|
+
Dynamic: author
|
|
48
|
+
Dynamic: home-page
|
|
49
|
+
Dynamic: license-file
|
|
50
|
+
Dynamic: requires-python
|
|
51
|
+
|
|
52
|
+
# PySMRF: High-Performance Parallel Simple Morphological Filter for LiDAR
|
|
53
|
+
|
|
54
|
+
[](https://www.python.org/)
|
|
55
|
+
[](https://opensource.org/licenses/MIT)
|
|
56
|
+
[](https://doi.org/10.1016/j.isprsjprs.2012.12.002)
|
|
57
|
+
|
|
58
|
+
**PySMRF** is a clean, modern, and highly optimized Python library implementing the **Simple Morphological Filter (SMRF)** algorithm for binary ground/object classification of airborne LiDAR point clouds and bare-earth Digital Elevation Model (DEM / DTM) generation.
|
|
59
|
+
|
|
60
|
+
This project is a complete rewrite of the original [`smrf`](https://github.com/thomaspingel/smrf) codebase by Thomas Pingel, re-engineered for **maximum performance**, **built-in multi-threaded/multi-process parallelism**, **native LAS/LAZ I/O**, and **production GIS pipelines**.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 📚 Academic Citation
|
|
65
|
+
|
|
66
|
+
If you use PySMRF in your academic research, geospatial workflows, or publications, **please cite the foundational paper**:
|
|
67
|
+
|
|
68
|
+
> **Pingel, T. J., Clarke, K. C., & McBride, W. A. (2013).**
|
|
69
|
+
> *An improved simple morphological filter for producing ground surfaces from LiDAR point clouds.*
|
|
70
|
+
> **ISPRS Journal of Photogrammetry and Remote Sensing**, 77, 21–30.
|
|
71
|
+
> [https://doi.org/10.1016/j.isprsjprs.2012.12.002](https://doi.org/10.1016/j.isprsjprs.2012.12.002)
|
|
72
|
+
|
|
73
|
+
### BibTeX
|
|
74
|
+
```bibtex
|
|
75
|
+
@article{pingel2013smrf,
|
|
76
|
+
title = {An improved simple morphological filter for producing ground surfaces from LiDAR point clouds},
|
|
77
|
+
author = {Pingel, Thomas J. and Clarke, Keith C. and McBride, William A.},
|
|
78
|
+
journal = {ISPRS Journal of Photogrammetry and Remote Sensing},
|
|
79
|
+
volume = {77},
|
|
80
|
+
pages = {21--30},
|
|
81
|
+
year = {2013},
|
|
82
|
+
publisher = {Elsevier},
|
|
83
|
+
doi = {10.1016/j.isprsjprs.2012.12.002},
|
|
84
|
+
url = {https://doi.org/10.1016/j.isprsjprs.2012.12.002}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 🚀 Key Improvements in PySMRF
|
|
91
|
+
|
|
92
|
+
| Feature | Original `smrf` | **PySMRF (Rewrite)** |
|
|
93
|
+
| :--- | :--- | :--- |
|
|
94
|
+
| **Grid DEM Creation** | Pandas `groupby.min()` (slow, heavy RAM) | **Vectorized C-level `np.minimum.at` (10–50x faster)** |
|
|
95
|
+
| **Parallelism** | None (single-threaded CPU only) | **Built-in multi-threading (`workers=-1`) with tiled spatial domain decomposition** |
|
|
96
|
+
| **Morphology Engine** | Sequential `skimage.morphology.opening` | **High-speed `scipy.ndimage.grey_opening` with cached disk footprints & parallel tiling** |
|
|
97
|
+
| **Point Interpolation** | `RectBivariateSpline` on full arrays | **Parallel chunked evaluation & optional bilinear `order=1`** |
|
|
98
|
+
| **LAS / LAZ Support** | Pure-Python custom reader (no LAZ) | **Unified `laspy` + `lazrs` integration with metadata/attribute preservation** |
|
|
99
|
+
| **Void Inpainting** | Uncached spring LSQR matrix | **Optimized sparse Laplacian network, FDA, and fast nearest/IDW solvers** |
|
|
100
|
+
| **API Design** | Monolithic script | **Modern functional API, OOP pipeline (`SMRF`), dataclass returns, & legacy compatibility** |
|
|
101
|
+
| **CLI Tool** | None | **Full-featured CLI: `pysmrf classify`, `pysmrf dem`, `pysmrf batch`, `pysmrf cite`** |
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 📦 Installation
|
|
106
|
+
|
|
107
|
+
### From Source
|
|
108
|
+
```bash
|
|
109
|
+
git clone https://github.com/thomaspingel/smrf.git
|
|
110
|
+
cd smrf
|
|
111
|
+
pip install -e .
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### With Optional Dependencies
|
|
115
|
+
```bash
|
|
116
|
+
# With LAZ compression support (lazrs)
|
|
117
|
+
pip install -e ".[laz]"
|
|
118
|
+
|
|
119
|
+
# With visualization support (matplotlib)
|
|
120
|
+
pip install -e ".[viz]"
|
|
121
|
+
|
|
122
|
+
# Complete installation with all optional features
|
|
123
|
+
pip install -e ".[all]"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## ⚡ Quickstart
|
|
129
|
+
|
|
130
|
+
### 1. Modern Functional API (`classify`)
|
|
131
|
+
```python
|
|
132
|
+
import pysmrf
|
|
133
|
+
|
|
134
|
+
# Classify points directly from arrays or file path
|
|
135
|
+
result = pysmrf.classify(
|
|
136
|
+
"survey.las",
|
|
137
|
+
cellsize=1.0,
|
|
138
|
+
windows=[1, 2, 3, 5, 10],
|
|
139
|
+
slope_threshold=0.15,
|
|
140
|
+
elevation_threshold=0.5,
|
|
141
|
+
elevation_scaler=1.25,
|
|
142
|
+
workers=-1, # Use all available CPU cores!
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
print(f"Total points: {result.num_points:,}")
|
|
146
|
+
print(f"Ground points: {result.ground_count:,} ({result.ground_percentage:.1f}%)")
|
|
147
|
+
print(f"Non-ground objects: {result.object_count:,}")
|
|
148
|
+
|
|
149
|
+
# Export bare-earth DTM raster and classified LAS
|
|
150
|
+
result.save_dem("bare_earth_dtm.tif")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 2. Object-Oriented Pipeline (`SMRF`)
|
|
154
|
+
```python
|
|
155
|
+
from pysmrf import SMRF
|
|
156
|
+
|
|
157
|
+
# Configure reusable filter
|
|
158
|
+
filter = SMRF(
|
|
159
|
+
cellsize=1.0,
|
|
160
|
+
windows=10,
|
|
161
|
+
slope_threshold=0.15,
|
|
162
|
+
elevation_threshold=0.4,
|
|
163
|
+
workers=8,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Process file directly, saving classified output and DTM GeoTIFF
|
|
167
|
+
result = filter.classify_file(
|
|
168
|
+
input_path="input.las",
|
|
169
|
+
output_path="classified.las",
|
|
170
|
+
save_dem_path="dtm.tif",
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 3. 100% Backward-Compatible Legacy Interface (`smrf.smrf`)
|
|
175
|
+
Existing code written for `smrf` works out-of-the-box:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
import pysmrf as smrf
|
|
179
|
+
|
|
180
|
+
# Exact legacy signature and return values
|
|
181
|
+
dtm, transform, object_cells, is_object_point = smrf.smrf(
|
|
182
|
+
x, y, z,
|
|
183
|
+
cellsize=1.0,
|
|
184
|
+
windows=5,
|
|
185
|
+
slope_threshold=0.15,
|
|
186
|
+
elevation_threshold=0.5,
|
|
187
|
+
elevation_scaler=1.25,
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 💻 Command-Line Interface (CLI)
|
|
194
|
+
|
|
195
|
+
PySMRF provides an intuitive CLI for processing files directly from the terminal:
|
|
196
|
+
|
|
197
|
+
### Classify a LAS/LAZ file
|
|
198
|
+
```bash
|
|
199
|
+
pysmrf classify input.las \
|
|
200
|
+
--output classified.las \
|
|
201
|
+
--dem bare_earth.tif \
|
|
202
|
+
--cellsize 1.0 \
|
|
203
|
+
--windows 1,2,3,5,10,15 \
|
|
204
|
+
--slope 0.15 \
|
|
205
|
+
--workers -1
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Generate a Bare-Earth DEM
|
|
209
|
+
```bash
|
|
210
|
+
pysmrf dem survey.laz -o dtm.tif --cellsize 1.0 --bin-type min
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Batch Process Multiple Files in Parallel
|
|
214
|
+
```bash
|
|
215
|
+
pysmrf batch "raw_tiles/*.las" -o classified_tiles/ --workers 8 --cellsize 1.0
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Inspect LiDAR File Metadata
|
|
219
|
+
```bash
|
|
220
|
+
pysmrf info input.las
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Print Formal Citation
|
|
224
|
+
```bash
|
|
225
|
+
pysmrf cite
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## ⚙️ Parameter Guide
|
|
231
|
+
|
|
232
|
+
| Parameter | Type | Default | Description |
|
|
233
|
+
| :--- | :--- | :--- | :--- |
|
|
234
|
+
| `cellsize` | `float` | `1.0` | Spatial resolution of the provisional grid in real-world map units (m or ft). |
|
|
235
|
+
| `windows` | `int` or `list` | `5` | Structuring element radii sequence. E.g., `5` evaluates `[1, 2, 3, 4, 5]`. Use larger radii (`15–30`) to remove large buildings; smaller radii (`2–5`) for small trees. |
|
|
236
|
+
| `slope_threshold` | `float` | `0.15` | Slope tolerance ($dz/dx$). `0.15` corresponds to 15% slope. Use higher values (`0.25–0.40`) in steep mountainous terrain. |
|
|
237
|
+
| `elevation_threshold`| `float` | `0.5` | Elevation distance tolerance from provisional DTM. Points within this distance are classified as ground. |
|
|
238
|
+
| `elevation_scaler` | `float` | `1.25` | Slope scaling factor: $\text{tol} = \text{elev\_thresh} + (\text{scaler} \times \text{slope})$. Set to `0` to disable slope scaling. |
|
|
239
|
+
| `low_filter_slope` | `float` | `5.0` | Threshold (500%) for identifying low outlier points from inverted surface. |
|
|
240
|
+
| `low_outlier_fill` | `bool` | `False` | Whether to inpaint and remove low outlier grid cells prior to progressive filtering. |
|
|
241
|
+
| `workers` | `int` | `1` | Number of worker threads. `-1` uses all available CPU cores. |
|
|
242
|
+
| `inpaint_method` | `str` | `'spring'`| Void inpainting algorithm: `'spring'` (Pingel 2013), `'fda'`, or `'nearest'`. |
|
|
243
|
+
| `interp_method` | `str` | `'spline'`| Surface interpolation method: `'spline'` (bivariate cubic spline) or `'linear'` (fast bilinear). |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## 🧪 Testing
|
|
248
|
+
|
|
249
|
+
Run the full pytest suite:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
pytest -v tests/
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 📄 License
|
|
258
|
+
|
|
259
|
+
This project is licensed under the [MIT License](LICENSE) - see the LICENSE file for details.
|
|
260
|
+
|
|
261
|
+
Original Algorithm & Prototype Copyright (c) 2013–2021 Thomas Pingel.
|
|
262
|
+
PySMRF Modern Rewrite Copyright (c) 2026 Aman Ranjan.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## 👤 Author & Maintainer
|
|
267
|
+
|
|
268
|
+
- **Aman Ranjan** ([er.amanranjan@gmail.com](mailto:er.amanranjan@gmail.com))
|
|
269
|
+
- **Thomas J. Pingel** (Original Algorithm & Prototype)
|
pysmrf-1.0.0/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# PySMRF: High-Performance Parallel Simple Morphological Filter for LiDAR
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://doi.org/10.1016/j.isprsjprs.2012.12.002)
|
|
6
|
+
|
|
7
|
+
**PySMRF** is a clean, modern, and highly optimized Python library implementing the **Simple Morphological Filter (SMRF)** algorithm for binary ground/object classification of airborne LiDAR point clouds and bare-earth Digital Elevation Model (DEM / DTM) generation.
|
|
8
|
+
|
|
9
|
+
This project is a complete rewrite of the original [`smrf`](https://github.com/thomaspingel/smrf) codebase by Thomas Pingel, re-engineered for **maximum performance**, **built-in multi-threaded/multi-process parallelism**, **native LAS/LAZ I/O**, and **production GIS pipelines**.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 📚 Academic Citation
|
|
14
|
+
|
|
15
|
+
If you use PySMRF in your academic research, geospatial workflows, or publications, **please cite the foundational paper**:
|
|
16
|
+
|
|
17
|
+
> **Pingel, T. J., Clarke, K. C., & McBride, W. A. (2013).**
|
|
18
|
+
> *An improved simple morphological filter for producing ground surfaces from LiDAR point clouds.*
|
|
19
|
+
> **ISPRS Journal of Photogrammetry and Remote Sensing**, 77, 21–30.
|
|
20
|
+
> [https://doi.org/10.1016/j.isprsjprs.2012.12.002](https://doi.org/10.1016/j.isprsjprs.2012.12.002)
|
|
21
|
+
|
|
22
|
+
### BibTeX
|
|
23
|
+
```bibtex
|
|
24
|
+
@article{pingel2013smrf,
|
|
25
|
+
title = {An improved simple morphological filter for producing ground surfaces from LiDAR point clouds},
|
|
26
|
+
author = {Pingel, Thomas J. and Clarke, Keith C. and McBride, William A.},
|
|
27
|
+
journal = {ISPRS Journal of Photogrammetry and Remote Sensing},
|
|
28
|
+
volume = {77},
|
|
29
|
+
pages = {21--30},
|
|
30
|
+
year = {2013},
|
|
31
|
+
publisher = {Elsevier},
|
|
32
|
+
doi = {10.1016/j.isprsjprs.2012.12.002},
|
|
33
|
+
url = {https://doi.org/10.1016/j.isprsjprs.2012.12.002}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🚀 Key Improvements in PySMRF
|
|
40
|
+
|
|
41
|
+
| Feature | Original `smrf` | **PySMRF (Rewrite)** |
|
|
42
|
+
| :--- | :--- | :--- |
|
|
43
|
+
| **Grid DEM Creation** | Pandas `groupby.min()` (slow, heavy RAM) | **Vectorized C-level `np.minimum.at` (10–50x faster)** |
|
|
44
|
+
| **Parallelism** | None (single-threaded CPU only) | **Built-in multi-threading (`workers=-1`) with tiled spatial domain decomposition** |
|
|
45
|
+
| **Morphology Engine** | Sequential `skimage.morphology.opening` | **High-speed `scipy.ndimage.grey_opening` with cached disk footprints & parallel tiling** |
|
|
46
|
+
| **Point Interpolation** | `RectBivariateSpline` on full arrays | **Parallel chunked evaluation & optional bilinear `order=1`** |
|
|
47
|
+
| **LAS / LAZ Support** | Pure-Python custom reader (no LAZ) | **Unified `laspy` + `lazrs` integration with metadata/attribute preservation** |
|
|
48
|
+
| **Void Inpainting** | Uncached spring LSQR matrix | **Optimized sparse Laplacian network, FDA, and fast nearest/IDW solvers** |
|
|
49
|
+
| **API Design** | Monolithic script | **Modern functional API, OOP pipeline (`SMRF`), dataclass returns, & legacy compatibility** |
|
|
50
|
+
| **CLI Tool** | None | **Full-featured CLI: `pysmrf classify`, `pysmrf dem`, `pysmrf batch`, `pysmrf cite`** |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 📦 Installation
|
|
55
|
+
|
|
56
|
+
### From Source
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/thomaspingel/smrf.git
|
|
59
|
+
cd smrf
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### With Optional Dependencies
|
|
64
|
+
```bash
|
|
65
|
+
# With LAZ compression support (lazrs)
|
|
66
|
+
pip install -e ".[laz]"
|
|
67
|
+
|
|
68
|
+
# With visualization support (matplotlib)
|
|
69
|
+
pip install -e ".[viz]"
|
|
70
|
+
|
|
71
|
+
# Complete installation with all optional features
|
|
72
|
+
pip install -e ".[all]"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## ⚡ Quickstart
|
|
78
|
+
|
|
79
|
+
### 1. Modern Functional API (`classify`)
|
|
80
|
+
```python
|
|
81
|
+
import pysmrf
|
|
82
|
+
|
|
83
|
+
# Classify points directly from arrays or file path
|
|
84
|
+
result = pysmrf.classify(
|
|
85
|
+
"survey.las",
|
|
86
|
+
cellsize=1.0,
|
|
87
|
+
windows=[1, 2, 3, 5, 10],
|
|
88
|
+
slope_threshold=0.15,
|
|
89
|
+
elevation_threshold=0.5,
|
|
90
|
+
elevation_scaler=1.25,
|
|
91
|
+
workers=-1, # Use all available CPU cores!
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
print(f"Total points: {result.num_points:,}")
|
|
95
|
+
print(f"Ground points: {result.ground_count:,} ({result.ground_percentage:.1f}%)")
|
|
96
|
+
print(f"Non-ground objects: {result.object_count:,}")
|
|
97
|
+
|
|
98
|
+
# Export bare-earth DTM raster and classified LAS
|
|
99
|
+
result.save_dem("bare_earth_dtm.tif")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. Object-Oriented Pipeline (`SMRF`)
|
|
103
|
+
```python
|
|
104
|
+
from pysmrf import SMRF
|
|
105
|
+
|
|
106
|
+
# Configure reusable filter
|
|
107
|
+
filter = SMRF(
|
|
108
|
+
cellsize=1.0,
|
|
109
|
+
windows=10,
|
|
110
|
+
slope_threshold=0.15,
|
|
111
|
+
elevation_threshold=0.4,
|
|
112
|
+
workers=8,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Process file directly, saving classified output and DTM GeoTIFF
|
|
116
|
+
result = filter.classify_file(
|
|
117
|
+
input_path="input.las",
|
|
118
|
+
output_path="classified.las",
|
|
119
|
+
save_dem_path="dtm.tif",
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 3. 100% Backward-Compatible Legacy Interface (`smrf.smrf`)
|
|
124
|
+
Existing code written for `smrf` works out-of-the-box:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
import pysmrf as smrf
|
|
128
|
+
|
|
129
|
+
# Exact legacy signature and return values
|
|
130
|
+
dtm, transform, object_cells, is_object_point = smrf.smrf(
|
|
131
|
+
x, y, z,
|
|
132
|
+
cellsize=1.0,
|
|
133
|
+
windows=5,
|
|
134
|
+
slope_threshold=0.15,
|
|
135
|
+
elevation_threshold=0.5,
|
|
136
|
+
elevation_scaler=1.25,
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 💻 Command-Line Interface (CLI)
|
|
143
|
+
|
|
144
|
+
PySMRF provides an intuitive CLI for processing files directly from the terminal:
|
|
145
|
+
|
|
146
|
+
### Classify a LAS/LAZ file
|
|
147
|
+
```bash
|
|
148
|
+
pysmrf classify input.las \
|
|
149
|
+
--output classified.las \
|
|
150
|
+
--dem bare_earth.tif \
|
|
151
|
+
--cellsize 1.0 \
|
|
152
|
+
--windows 1,2,3,5,10,15 \
|
|
153
|
+
--slope 0.15 \
|
|
154
|
+
--workers -1
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Generate a Bare-Earth DEM
|
|
158
|
+
```bash
|
|
159
|
+
pysmrf dem survey.laz -o dtm.tif --cellsize 1.0 --bin-type min
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Batch Process Multiple Files in Parallel
|
|
163
|
+
```bash
|
|
164
|
+
pysmrf batch "raw_tiles/*.las" -o classified_tiles/ --workers 8 --cellsize 1.0
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Inspect LiDAR File Metadata
|
|
168
|
+
```bash
|
|
169
|
+
pysmrf info input.las
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Print Formal Citation
|
|
173
|
+
```bash
|
|
174
|
+
pysmrf cite
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## ⚙️ Parameter Guide
|
|
180
|
+
|
|
181
|
+
| Parameter | Type | Default | Description |
|
|
182
|
+
| :--- | :--- | :--- | :--- |
|
|
183
|
+
| `cellsize` | `float` | `1.0` | Spatial resolution of the provisional grid in real-world map units (m or ft). |
|
|
184
|
+
| `windows` | `int` or `list` | `5` | Structuring element radii sequence. E.g., `5` evaluates `[1, 2, 3, 4, 5]`. Use larger radii (`15–30`) to remove large buildings; smaller radii (`2–5`) for small trees. |
|
|
185
|
+
| `slope_threshold` | `float` | `0.15` | Slope tolerance ($dz/dx$). `0.15` corresponds to 15% slope. Use higher values (`0.25–0.40`) in steep mountainous terrain. |
|
|
186
|
+
| `elevation_threshold`| `float` | `0.5` | Elevation distance tolerance from provisional DTM. Points within this distance are classified as ground. |
|
|
187
|
+
| `elevation_scaler` | `float` | `1.25` | Slope scaling factor: $\text{tol} = \text{elev\_thresh} + (\text{scaler} \times \text{slope})$. Set to `0` to disable slope scaling. |
|
|
188
|
+
| `low_filter_slope` | `float` | `5.0` | Threshold (500%) for identifying low outlier points from inverted surface. |
|
|
189
|
+
| `low_outlier_fill` | `bool` | `False` | Whether to inpaint and remove low outlier grid cells prior to progressive filtering. |
|
|
190
|
+
| `workers` | `int` | `1` | Number of worker threads. `-1` uses all available CPU cores. |
|
|
191
|
+
| `inpaint_method` | `str` | `'spring'`| Void inpainting algorithm: `'spring'` (Pingel 2013), `'fda'`, or `'nearest'`. |
|
|
192
|
+
| `interp_method` | `str` | `'spline'`| Surface interpolation method: `'spline'` (bivariate cubic spline) or `'linear'` (fast bilinear). |
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 🧪 Testing
|
|
197
|
+
|
|
198
|
+
Run the full pytest suite:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
pytest -v tests/
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 📄 License
|
|
207
|
+
|
|
208
|
+
This project is licensed under the [MIT License](LICENSE) - see the LICENSE file for details.
|
|
209
|
+
|
|
210
|
+
Original Algorithm & Prototype Copyright (c) 2013–2021 Thomas Pingel.
|
|
211
|
+
PySMRF Modern Rewrite Copyright (c) 2026 Aman Ranjan.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 👤 Author & Maintainer
|
|
216
|
+
|
|
217
|
+
- **Aman Ranjan** ([er.amanranjan@gmail.com](mailto:er.amanranjan@gmail.com))
|
|
218
|
+
- **Thomas J. Pingel** (Original Algorithm & Prototype)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pysmrf"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "High-performance parallel Simple Morphological Filter (SMRF) for LiDAR ground classification and DEM generation citing Pingel et al. (2013)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Aman Ranjan", email = "er.amanranjan@gmail.com" },
|
|
13
|
+
]
|
|
14
|
+
maintainers = [
|
|
15
|
+
{ name = "Aman Ranjan", email = "er.amanranjan@gmail.com" }
|
|
16
|
+
]
|
|
17
|
+
keywords = ["lidar", "point-cloud", "gis", "dem", "dtm", "morphological-filter", "ground-classification", "parallel", "remote-sensing"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 5 - Production/Stable",
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.9",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Operating System :: OS Independent"
|
|
31
|
+
]
|
|
32
|
+
requires-python = ">=3.9"
|
|
33
|
+
dependencies = [
|
|
34
|
+
"numpy>=1.22.0",
|
|
35
|
+
"scipy>=1.8.0",
|
|
36
|
+
"rasterio>=1.3.0",
|
|
37
|
+
"pandas>=1.4.0",
|
|
38
|
+
"click>=8.0.0",
|
|
39
|
+
"laspy[laszip,lazrs]>=2.6.1",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
viz = [
|
|
44
|
+
"matplotlib>=3.5.0"
|
|
45
|
+
]
|
|
46
|
+
all = [
|
|
47
|
+
"lazrs>=0.5.0",
|
|
48
|
+
"matplotlib>=3.5.0",
|
|
49
|
+
"scikit-image>=0.19.0"
|
|
50
|
+
]
|
|
51
|
+
dev = [
|
|
52
|
+
"pytest>=7.0.0",
|
|
53
|
+
"pytest-cov>=4.0.0",
|
|
54
|
+
"black>=23.0.0",
|
|
55
|
+
"ruff>=0.1.0",
|
|
56
|
+
"lazrs>=0.5.0"
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[project.urls]
|
|
60
|
+
Homepage = "https://github.com/thomaspingel/smrf"
|
|
61
|
+
Paper = "https://doi.org/10.1016/j.isprsjprs.2012.12.002"
|
|
62
|
+
Repository = "https://github.com/thomaspingel/smrf"
|
|
63
|
+
|
|
64
|
+
[project.scripts]
|
|
65
|
+
pysmrf = "pysmrf.cli:cli"
|
|
66
|
+
|
|
67
|
+
[tool.setuptools.packages.find]
|
|
68
|
+
where = ["src"]
|
|
69
|
+
include = ["pysmrf*"]
|
|
70
|
+
|
|
71
|
+
[tool.pytest.ini_options]
|
|
72
|
+
pythonpath = ["src", "tests"]
|
|
73
|
+
testpaths = ["tests"]
|
pysmrf-1.0.0/setup.cfg
ADDED