easy-eo 0.1.0__py3-none-any.whl
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.
- easy_eo-0.1.0.dist-info/METADATA +114 -0
- easy_eo-0.1.0.dist-info/RECORD +40 -0
- easy_eo-0.1.0.dist-info/WHEEL +4 -0
- easy_eo-0.1.0.dist-info/licenses/LICENSE +21 -0
- eeo/__init__.py +20 -0
- eeo/analysis/__init__.py +13 -0
- eeo/analysis/indices.py +62 -0
- eeo/analysis/indices.pyi +35 -0
- eeo/analysis/stats.py +127 -0
- eeo/analysis/stats.pyi +99 -0
- eeo/common.py +35 -0
- eeo/core/__init__.py +11 -0
- eeo/core/adapters/__init__.py +10 -0
- eeo/core/adapters/base.py +94 -0
- eeo/core/adapters/numpy.py +102 -0
- eeo/core/adapters/rasterio.py +131 -0
- eeo/core/core.py +198 -0
- eeo/core/core.pyi +955 -0
- eeo/core/decorators.py +52 -0
- eeo/core/loader.py +42 -0
- eeo/core/loader.pyi +41 -0
- eeo/core/plugins.py +15 -0
- eeo/core/types.py +17 -0
- eeo/ops/__init__.py +15 -0
- eeo/ops/algebra.py +170 -0
- eeo/ops/algebra.pyi +175 -0
- eeo/ops/merge.py +133 -0
- eeo/ops/merge.pyi +46 -0
- eeo/preprocessing/__init__.py +14 -0
- eeo/preprocessing/clip.py +136 -0
- eeo/preprocessing/clip.pyi +83 -0
- eeo/preprocessing/normalize.py +50 -0
- eeo/preprocessing/normalize.pyi +67 -0
- eeo/preprocessing/reproject.py +73 -0
- eeo/preprocessing/reproject.pyi +36 -0
- eeo/preprocessing/resample.py +81 -0
- eeo/preprocessing/resample.pyi +44 -0
- eeo/viz/__init__.py +9 -0
- eeo/viz/plot.py +284 -0
- eeo/viz/plot.pyi +163 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easy-eo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight Earth Observation utilities library for chainable raster operations
|
|
5
|
+
Project-URL: Homepage, https://github.com/tommy-burns/easy-eo
|
|
6
|
+
Project-URL: Repository, https://github.com/tommy-burns/easy-eo
|
|
7
|
+
Project-URL: Documentation, https://easy-eo.readthedocs.io
|
|
8
|
+
Project-URL: Issues, https://github.com/tommy-burns/easy-eo/issues
|
|
9
|
+
Author: Thomas Burns Botchwey
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2025 Thomas Burns Botchwey
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: earth observation,gis,raster,remote sensing
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Intended Audience :: Science/Research
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
41
|
+
Requires-Python: >=3.10
|
|
42
|
+
Requires-Dist: geopandas<1.2,>=1.1
|
|
43
|
+
Requires-Dist: matplotlib<3.11,>=3.10
|
|
44
|
+
Requires-Dist: numpy<2.2,>=1.26
|
|
45
|
+
Requires-Dist: rasterio<1.5,>=1.4
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# EasyEO
|
|
49
|
+
|
|
50
|
+
**EasyEO** is a lightweight, extensible Python library for raster-based Earth Observation (EO) analysis which allows for chainable raster processing, algebra, and visualization.
|
|
51
|
+
It provides high-level abstractions over libraries such as Rasterio, NumPy, and Matplotlib, enabling users to perform common earth-observation analyses and visualization tasks efficiently, without dealing with the underlying complexity.
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
- Raster operations with spatial awareness
|
|
57
|
+
- Algebraic operations (`add`, `subtract`, `multiply`, `divide`)
|
|
58
|
+
- Resampling, reprojection, and alignment
|
|
59
|
+
- Clip rasters using vectors or bounding boxes
|
|
60
|
+
- Normalization (min-max, percentile, z-score)
|
|
61
|
+
- Visualization helpers (bands, composites, histograms)
|
|
62
|
+
- Backend-aware design (NumPy ↔ Rasterio)
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
conda create -n env_name python=3.10
|
|
70
|
+
conda activate env_name
|
|
71
|
+
pip install easy-eo
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Quick Example
|
|
75
|
+
```python
|
|
76
|
+
from eeo import load_raster
|
|
77
|
+
|
|
78
|
+
ds_nir = load_raster("path/to/nir.tif")
|
|
79
|
+
ds_red = load_raster("path/to/red.tif")
|
|
80
|
+
|
|
81
|
+
# Chainable example: clip -> resample -> compute NDVI -> multiply
|
|
82
|
+
result = ds_nir.clip_raster_with_bbox((0,0,1000,1000))
|
|
83
|
+
.resample(scale_factor=2)
|
|
84
|
+
.normalized_difference(ds_red)
|
|
85
|
+
.multiply(100)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Supported Backends
|
|
89
|
+
| Backend | Description |
|
|
90
|
+
|----------|------------------------------------------------------|
|
|
91
|
+
| NumPy | Fast, in-memory arrays without I/O |
|
|
92
|
+
| Rasterio | Full geospatial support (CRS, transform, resampling) |
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## Documentation
|
|
96
|
+
|
|
97
|
+
📚 Full documentation is available at:
|
|
98
|
+
|
|
99
|
+
👉 https://easyeo.readthedocs.io
|
|
100
|
+
|
|
101
|
+
## Project Status
|
|
102
|
+
🚧 Active development
|
|
103
|
+
The API is stabilizing but may change before v1.0.
|
|
104
|
+
|
|
105
|
+
## Contributing
|
|
106
|
+
Contributions are welcome!
|
|
107
|
+
- Bug reports
|
|
108
|
+
- Feature requests
|
|
109
|
+
- Documentation improvements
|
|
110
|
+
|
|
111
|
+
Please open an issue or pull request on GitHub.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
MIT License © 2025 Thomas Burns Botchwey
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
eeo/__init__.py,sha256=Mfax2GYSEmqvaoLGLuuU3XGIn8LAL1HEn3NKgFLILOc,338
|
|
2
|
+
eeo/common.py,sha256=nFVIvcTv1QtEw2sRRbmb3O1JIh_kSkKmla4TT6E3cBA,1438
|
|
3
|
+
eeo/analysis/__init__.py,sha256=dMdhRbd-DFteo-buxAUAw-JRjEnNByylyElPyCqmigk,346
|
|
4
|
+
eeo/analysis/indices.py,sha256=HWezvp22vwBbpaKV-uJYMkOWbcmysKaWq9eZ7FFmF0Q,1740
|
|
5
|
+
eeo/analysis/indices.pyi,sha256=VHXJz0G73TLZkeqPoTK31SzMVxsvGv_shFXlilH1MIU,1022
|
|
6
|
+
eeo/analysis/stats.py,sha256=OePUexlDqtHm5Yw-W6fUI4ywbH6CyCgMokVmwz77E8U,3366
|
|
7
|
+
eeo/analysis/stats.pyi,sha256=gadY8wnxI4tcmDk1frqT6oi2pkSyOLa4zwss9BXiJqM,3109
|
|
8
|
+
eeo/core/__init__.py,sha256=vGae3Ge5jD-FtlaKSH4DApmao8O610PATfFMw7QquCE,197
|
|
9
|
+
eeo/core/core.py,sha256=PGAT_EiP5x5tBKUlnwkOhYA28frJgVgUacjo7wInsEE,5154
|
|
10
|
+
eeo/core/core.pyi,sha256=-1kgxHcyyh0cMik4lMQdbK7-3gBmwtggnPO_9R5phBY,30352
|
|
11
|
+
eeo/core/decorators.py,sha256=ihRkRe5AbwID29_iVTnYaEnuFhMyUnsjonwreeUZILQ,1581
|
|
12
|
+
eeo/core/loader.py,sha256=zuuM5MyclPlFbbJ336LrYU5gIGIbau8NRg_jow2fpGE,1156
|
|
13
|
+
eeo/core/loader.pyi,sha256=ZHFkWqVd7B_5VE2zUxOMdIWhqwGdPVwKfpKJwHhhGxM,1356
|
|
14
|
+
eeo/core/plugins.py,sha256=p1Iwd8T4xWkNLY8k-Lliuz_EuZW6EMncwVULpz0KVW8,478
|
|
15
|
+
eeo/core/types.py,sha256=N6O7-oonAeQP44oZDQ1C3lVbyfTX8LnaPjbtt-ec6ck,273
|
|
16
|
+
eeo/core/adapters/__init__.py,sha256=DcVkSYDDrhey07SBaeE1-tXuzyXTzwLL37sk5YQ35xk,206
|
|
17
|
+
eeo/core/adapters/base.py,sha256=rA5TlG5e9e1TAI3rtd-VmLPp0SxCbNtDV2pCLoroOrs,2055
|
|
18
|
+
eeo/core/adapters/numpy.py,sha256=jxPKF8WjCaHfP62uPwE1DAuCEPlmg57PXDFrC0z_F14,2724
|
|
19
|
+
eeo/core/adapters/rasterio.py,sha256=cphA4i1iaBbP_ly4xYaxSvUfZ5hIUJxlusx-FmJWJy8,3353
|
|
20
|
+
eeo/ops/__init__.py,sha256=WYkRfGRu0_memC-jCJAIGb1RbK1SJvLy6P5zHpA1kMU,264
|
|
21
|
+
eeo/ops/algebra.py,sha256=HivIT9e9rg4Ifmqh-X8x6mAAV-5I9BKLSDksRsNnC-0,5686
|
|
22
|
+
eeo/ops/algebra.pyi,sha256=3GKjNxz6c5eEu7S4aiKyV7mUBbgy8cMJcnQBi9xjXMg,3906
|
|
23
|
+
eeo/ops/merge.py,sha256=i6ajEEwyYjbwcbIp8mm9N3QkomQkz-q3Po02ySAqOGE,3789
|
|
24
|
+
eeo/ops/merge.pyi,sha256=KDN5hkpBPVfJmpork02glmSIZ-4YAhuGUqU1vMCNb60,1197
|
|
25
|
+
eeo/preprocessing/__init__.py,sha256=pCT2etB3U5LJUwvYaiqC-J-MvlKZio3Mx_MwvPFiUWs,399
|
|
26
|
+
eeo/preprocessing/clip.py,sha256=9NWHF9XOGiAhHo3OJ-Kkfr9ney2xpaYeTRx7QRfYo4A,3657
|
|
27
|
+
eeo/preprocessing/clip.pyi,sha256=twOgFrKReyqBNl_ny8cxkHYbf0iYNc1fQBtGrH6eZ8U,3234
|
|
28
|
+
eeo/preprocessing/normalize.py,sha256=uCktIlMxdlWiC6IARAlBoZGKETuri6avHcjED3EUZM4,1829
|
|
29
|
+
eeo/preprocessing/normalize.pyi,sha256=uX_jMCd2M-nyqAwE5pLXh9rY43cMn5Y7lZdIxKiOlWU,1499
|
|
30
|
+
eeo/preprocessing/reproject.py,sha256=amHfZ438fY36u8OE1CaZ-zeHK1BqV841lIiSL5lBuz4,2163
|
|
31
|
+
eeo/preprocessing/reproject.pyi,sha256=7ptnJGj9ynA2j1VCCBxODufNfpydCoWTGpR3UiUJSYc,1020
|
|
32
|
+
eeo/preprocessing/resample.py,sha256=jFhNvUOZDrtLNNUlH2CI_BXi3SU0dmbVaoYvxgRfx1s,2726
|
|
33
|
+
eeo/preprocessing/resample.pyi,sha256=nEqtsNnwv4TqNi49RXuG6NWYitCJ6jeL0TOCg-r_nNw,2143
|
|
34
|
+
eeo/viz/__init__.py,sha256=-b320FRePvx2D7T-E0QX8zItgzICsCdPQtZjOs_bdAE,241
|
|
35
|
+
eeo/viz/plot.py,sha256=2fgFm8grI1DR91cwAN1CcGoLw3eoO0qs3TbYbvox-l8,8078
|
|
36
|
+
eeo/viz/plot.pyi,sha256=0zsNc8YYOwHJGrQlhSUvNcm_vSw9opAMr2f94IR3gaE,5272
|
|
37
|
+
easy_eo-0.1.0.dist-info/METADATA,sha256=qdNvabcqefd0xjbV93uiKeMXf6zxCGUz4DjUE9ghfNs,4335
|
|
38
|
+
easy_eo-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
39
|
+
easy_eo-0.1.0.dist-info/licenses/LICENSE,sha256=Swjd_zi9gT_xRe3AfsAE4r7aMoCl_B5hGJNiEcNHmow,1078
|
|
40
|
+
easy_eo-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Thomas Burns Botchwey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
eeo/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Easy-EO
|
|
3
|
+
A lightweight Earth Observation utilities library for chainable raster operations
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from .core import load_raster, load_array
|
|
8
|
+
from .analysis import *
|
|
9
|
+
from .ops import *
|
|
10
|
+
from .viz import *
|
|
11
|
+
from .preprocessing import *
|
|
12
|
+
from .core.adapters import *
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"load_raster",
|
|
17
|
+
"load_array",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
eeo/analysis/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .stats import extract_value_at_coordinate, get_maximum_pixel, get_mean_pixel, get_minimum_pixel, get_percentile_pixel
|
|
2
|
+
|
|
3
|
+
from .indices import normalized_difference
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"extract_value_at_coordinate",
|
|
8
|
+
"normalized_difference",
|
|
9
|
+
"get_minimum_pixel",
|
|
10
|
+
"get_percentile_pixel",
|
|
11
|
+
"get_mean_pixel",
|
|
12
|
+
"get_maximum_pixel"
|
|
13
|
+
]
|
eeo/analysis/indices.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This library exposes algebraic primitives instead of predefined indices.
|
|
3
|
+
Most vegetation and water indices can be expressed directly using
|
|
4
|
+
``normalized_difference`` or raster arithmetic.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import rasterio as rio
|
|
11
|
+
|
|
12
|
+
from eeo.common import align_raster_to_target
|
|
13
|
+
from eeo.core.core import EEORasterDataset
|
|
14
|
+
from eeo.core.decorators import eeo_raster_op
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@eeo_raster_op
|
|
18
|
+
def normalized_difference(
|
|
19
|
+
ds: EEORasterDataset,
|
|
20
|
+
other: EEORasterDataset,
|
|
21
|
+
*,
|
|
22
|
+
auto_align: bool = True,
|
|
23
|
+
method: str = "bilinear",
|
|
24
|
+
return_as_ndarray: bool = False
|
|
25
|
+
) -> Union[np.ndarray, EEORasterDataset]:
|
|
26
|
+
# Ensure reprojection for only rasterio-backend datasets
|
|
27
|
+
backend = ds._adapter.backend
|
|
28
|
+
if not isinstance(backend, rio.DatasetReader):
|
|
29
|
+
ds = ds.to_rasterio()
|
|
30
|
+
if ds.get_shape() != other.get_shape() or ds.get_transform() != other.get_transform():
|
|
31
|
+
if auto_align:
|
|
32
|
+
other = align_raster_to_target(other, ds, method=method)
|
|
33
|
+
else:
|
|
34
|
+
raise ValueError("Rasters must have the same shape and alignment")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
a = ds.read().astype(rio.float32)
|
|
38
|
+
b = other.read().astype(rio.float32)
|
|
39
|
+
|
|
40
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
41
|
+
nd = (a - b) / (a + b)
|
|
42
|
+
nd[np.isnan(nd)] = 0
|
|
43
|
+
|
|
44
|
+
if return_as_ndarray:
|
|
45
|
+
return nd
|
|
46
|
+
|
|
47
|
+
meta = ds.get_metadata().copy()
|
|
48
|
+
|
|
49
|
+
# Ensure correct metadata for writing
|
|
50
|
+
meta.update(
|
|
51
|
+
driver="GTiff",
|
|
52
|
+
dtype="float32",
|
|
53
|
+
height=nd.shape[-2],
|
|
54
|
+
width=nd.shape[-1],
|
|
55
|
+
count=nd.shape[0],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
memfile = rio.io.MemoryFile()
|
|
59
|
+
out_ds = memfile.open(**meta)
|
|
60
|
+
out_ds.write(nd)
|
|
61
|
+
|
|
62
|
+
return EEORasterDataset.from_rasterio(out_ds)
|
eeo/analysis/indices.pyi
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from eeo.core.core import EEORasterDataset
|
|
6
|
+
from eeo.core.decorators import eeo_raster_op
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@eeo_raster_op
|
|
10
|
+
def normalized_difference(
|
|
11
|
+
ds: EEORasterDataset,
|
|
12
|
+
other: EEORasterDataset,
|
|
13
|
+
*,
|
|
14
|
+
auto_align: bool = True,
|
|
15
|
+
method: str = "bilinear",
|
|
16
|
+
return_as_ndarray: bool = False
|
|
17
|
+
) -> Union[np.ndarray, EEORasterDataset]:
|
|
18
|
+
"""
|
|
19
|
+
Compute the normalized difference index: (ds - other) / (ds + other)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
:param ds: EEORasterDataset
|
|
23
|
+
First raster (e.g. NIR).
|
|
24
|
+
:param other: EEORasterDataset
|
|
25
|
+
Second raster (e.g. Red).
|
|
26
|
+
:param auto_align: bool, optional
|
|
27
|
+
Automatically resample `other` to match `ds`.
|
|
28
|
+
:param method: str, optional
|
|
29
|
+
Resampling method used if auto-aligning.
|
|
30
|
+
:param return_as_ndarray: bool, optional
|
|
31
|
+
If True, return a NumPy array instead of an EEORasterDataset.
|
|
32
|
+
|
|
33
|
+
:return: Union[np.ndarray, EEORasterDataset]
|
|
34
|
+
"""
|
|
35
|
+
...
|
eeo/analysis/stats.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import rasterio as rio
|
|
5
|
+
from eeo.common import mask_nodata
|
|
6
|
+
from eeo.core.core import EEORasterDataset
|
|
7
|
+
from eeo.core.decorators import eeo_raster_op
|
|
8
|
+
|
|
9
|
+
Coordinate = Union[tuple[float, float], list[float]]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@eeo_raster_op
|
|
13
|
+
def extract_value_at_coordinate(
|
|
14
|
+
ds: EEORasterDataset,
|
|
15
|
+
coordinates: Coordinate,
|
|
16
|
+
band_idx: int = 1
|
|
17
|
+
) -> Union[int, float]:
|
|
18
|
+
|
|
19
|
+
if len(coordinates) != 2:
|
|
20
|
+
raise ValueError(f"Expected 2 coordinates, got {len(coordinates)}")
|
|
21
|
+
|
|
22
|
+
backend = ds._adapter.backend
|
|
23
|
+
if not isinstance(backend, rio.DatasetReader):
|
|
24
|
+
ds = ds.to_rasterio()
|
|
25
|
+
backend = ds._adapter.backend
|
|
26
|
+
|
|
27
|
+
x, y = coordinates
|
|
28
|
+
row, col = backend.index(x, y)
|
|
29
|
+
|
|
30
|
+
return ds.get_band(band_idx)[row, col]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@eeo_raster_op
|
|
35
|
+
def get_maximum_pixel(
|
|
36
|
+
ds: EEORasterDataset,
|
|
37
|
+
band_idx: int = 1,
|
|
38
|
+
*,
|
|
39
|
+
return_position_as_pixel_coordinate: bool = False,
|
|
40
|
+
) -> dict:
|
|
41
|
+
band = ds.read() if ds.get_count() == 1 else ds.get_band(band_idx)
|
|
42
|
+
band = mask_nodata(ds, band)
|
|
43
|
+
|
|
44
|
+
# get max value
|
|
45
|
+
value = float(np.nanmax(band))
|
|
46
|
+
_, row, col = np.unravel_index(np.nanargmax(band), band.shape)
|
|
47
|
+
|
|
48
|
+
if return_position_as_pixel_coordinate:
|
|
49
|
+
position = (row, col)
|
|
50
|
+
else:
|
|
51
|
+
transform = ds.get_transform()
|
|
52
|
+
position = transform * (col, row)
|
|
53
|
+
|
|
54
|
+
return {"value": value, "position": position}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@eeo_raster_op
|
|
58
|
+
def get_minimum_pixel(
|
|
59
|
+
ds: EEORasterDataset,
|
|
60
|
+
band_idx: int = 1,
|
|
61
|
+
*,
|
|
62
|
+
return_position_as_pixel_coordinate: bool = False,
|
|
63
|
+
) -> dict:
|
|
64
|
+
band = ds.read() if ds.get_count() == 1 else ds.get_band(band_idx)
|
|
65
|
+
band = mask_nodata(ds, band)
|
|
66
|
+
|
|
67
|
+
# get min value
|
|
68
|
+
value = float(np.nanmin(band))
|
|
69
|
+
_, row, col = np.unravel_index(np.nanargmin(band), band.shape)
|
|
70
|
+
|
|
71
|
+
if return_position_as_pixel_coordinate:
|
|
72
|
+
position = (row, col)
|
|
73
|
+
else:
|
|
74
|
+
transform = ds.get_transform()
|
|
75
|
+
position = transform * (col, row)
|
|
76
|
+
|
|
77
|
+
return {"value": value, "position": position}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@eeo_raster_op
|
|
81
|
+
def get_mean_pixel(
|
|
82
|
+
ds: EEORasterDataset,
|
|
83
|
+
band_idx: int = 1,
|
|
84
|
+
*,
|
|
85
|
+
return_position_as_pixel_coordinate: bool = False,
|
|
86
|
+
) -> dict:
|
|
87
|
+
band = ds.read() if ds.get_count() == 1 else ds.get_band(band_idx)
|
|
88
|
+
band = mask_nodata(ds, band)
|
|
89
|
+
|
|
90
|
+
mean_value = float(np.nanmean(band))
|
|
91
|
+
diff = np.abs(band - mean_value)
|
|
92
|
+
|
|
93
|
+
_, row, col = np.unravel_index(np.nanargmin(diff), diff.shape)
|
|
94
|
+
|
|
95
|
+
if return_position_as_pixel_coordinate:
|
|
96
|
+
position = (row, col)
|
|
97
|
+
else:
|
|
98
|
+
transform = ds.get_transform()
|
|
99
|
+
position = transform * (col, row)
|
|
100
|
+
|
|
101
|
+
return {"value": mean_value, "position": position}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@eeo_raster_op
|
|
105
|
+
def get_percentile_pixel(
|
|
106
|
+
ds: EEORasterDataset,
|
|
107
|
+
percentile: float,
|
|
108
|
+
band_idx: int = 1,
|
|
109
|
+
*,
|
|
110
|
+
return_position_as_pixel_coordinate: bool = False,
|
|
111
|
+
) -> dict:
|
|
112
|
+
band = ds.read() if ds.get_count() == 1 else ds.get_band(band_idx)
|
|
113
|
+
band = mask_nodata(ds, band)
|
|
114
|
+
|
|
115
|
+
perc_value = float(np.nanpercentile(band, percentile))
|
|
116
|
+
diff = np.abs(band - perc_value)
|
|
117
|
+
|
|
118
|
+
_, row, col = np.unravel_index(np.nanargmin(diff), band.shape)
|
|
119
|
+
|
|
120
|
+
if return_position_as_pixel_coordinate:
|
|
121
|
+
position = (row, col)
|
|
122
|
+
else:
|
|
123
|
+
transform = ds.get_transform()
|
|
124
|
+
position = transform * (col, row)
|
|
125
|
+
|
|
126
|
+
return {"value": perc_value, "position": position}
|
|
127
|
+
|
eeo/analysis/stats.pyi
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
from eeo.core.core import EEORasterDataset
|
|
4
|
+
|
|
5
|
+
Coordinate = Union[tuple[float, float], list[float]]
|
|
6
|
+
|
|
7
|
+
def extract_value_at_coordinate(
|
|
8
|
+
ds: EEORasterDataset,
|
|
9
|
+
coordinates: Coordinate,
|
|
10
|
+
band_idx: int = 1,
|
|
11
|
+
) -> Union[int, float]:
|
|
12
|
+
"""
|
|
13
|
+
Extract the pixel value from an EEORasterDataset at a given geographic coordinate.
|
|
14
|
+
|
|
15
|
+
:param ds: The raster dataset from which to extract a value.
|
|
16
|
+
:type ds: EEORasterDataset
|
|
17
|
+
:param coordinates: A tuple of (x, y) coordinates. Must contain exactly two values.
|
|
18
|
+
:type coordinates: Coordinate
|
|
19
|
+
:param band_idx: The index of the raster band to sample from in case of a multiband raster.
|
|
20
|
+
:type band_idx: int
|
|
21
|
+
|
|
22
|
+
:return: The extracted pixel value from the specified band and coordinate.
|
|
23
|
+
:rtype: int or float
|
|
24
|
+
|
|
25
|
+
:raises ValueError: If the provided coordinates do not contain exactly two elements.
|
|
26
|
+
"""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_maximum_pixel(
|
|
31
|
+
ds: EEORasterDataset,
|
|
32
|
+
band_idx: int = 1,
|
|
33
|
+
*,
|
|
34
|
+
return_position_as_pixel_coordinate: bool = False,
|
|
35
|
+
) -> dict:
|
|
36
|
+
"""
|
|
37
|
+
Get the maximum pixel value and its location from a raster band.
|
|
38
|
+
|
|
39
|
+
:param ds: Input raster dataset.
|
|
40
|
+
:param band_idx: Band index to evaluate (1-based).
|
|
41
|
+
:param return_position_as_pixel_coordinate: If True, return (row, col) instead of spatial coordinates.
|
|
42
|
+
|
|
43
|
+
:return: Dictionary containing the maximum value and its position.
|
|
44
|
+
:rtype: dict[str, float | tuple]
|
|
45
|
+
"""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def get_minimum_pixel(
|
|
49
|
+
ds: EEORasterDataset,
|
|
50
|
+
band_idx: int = 1,
|
|
51
|
+
*,
|
|
52
|
+
return_position_as_pixel_coordinate: bool = False,
|
|
53
|
+
) -> dict:
|
|
54
|
+
"""
|
|
55
|
+
Get the minimum pixel value and its location from a raster band.
|
|
56
|
+
|
|
57
|
+
:param ds: Input raster dataset.
|
|
58
|
+
:param band_idx: Band index to evaluate (1-based).
|
|
59
|
+
:param return_position_as_pixel_coordinate: If True, return (row, col) instead of spatial coordinates.
|
|
60
|
+
|
|
61
|
+
:return: Dictionary containing the minimum value and its position.
|
|
62
|
+
:rtype: dict[str, float | tuple]
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
def get_mean_pixel(
|
|
67
|
+
ds: EEORasterDataset,
|
|
68
|
+
band_idx: int = 1,
|
|
69
|
+
*,
|
|
70
|
+
return_position_as_pixel_coordinate: bool = False,
|
|
71
|
+
) -> dict:
|
|
72
|
+
"""
|
|
73
|
+
Get the pixel whose value is closest to the mean of the raster band.
|
|
74
|
+
|
|
75
|
+
:param ds: Input raster dataset.
|
|
76
|
+
:param band_idx: Band index to evaluate (1-based).
|
|
77
|
+
:param return_position_as_pixel_coordinate: If True, return (row, col) instead of spatial coordinates.
|
|
78
|
+
|
|
79
|
+
:return: Dictionary containing the mean value and its position.
|
|
80
|
+
"""
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
def get_percentile_pixel(
|
|
84
|
+
ds: EEORasterDataset,
|
|
85
|
+
percentile: float,
|
|
86
|
+
band_idx: int = 1,
|
|
87
|
+
*,
|
|
88
|
+
return_position_as_pixel_coordinate: bool = False,
|
|
89
|
+
) -> dict:
|
|
90
|
+
"""
|
|
91
|
+
Get the pixel corresponding to a given percentile of raster values.
|
|
92
|
+
|
|
93
|
+
:param ds: Input raster dataset.
|
|
94
|
+
:param percentile: Percentile in the range [0, 100].
|
|
95
|
+
:param band_idx: Band index to evaluate (1-based).
|
|
96
|
+
:param return_position_as_pixel_coordinate: If True, return (row, col) instead of spatial coordinates.
|
|
97
|
+
:return: Dictionary containing the percentile value and its position.
|
|
98
|
+
"""
|
|
99
|
+
...
|
eeo/common.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from rasterio.enums import Resampling
|
|
3
|
+
|
|
4
|
+
from eeo.core.core import EEORasterDataset
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def normalize_resampling_method(value):
|
|
8
|
+
"""Function to normalize resampling method either user passes a rasterio.enums.Resampling or a string"""
|
|
9
|
+
if isinstance(value, Resampling):
|
|
10
|
+
return value
|
|
11
|
+
if isinstance(value, str):
|
|
12
|
+
name = value.lower().strip()
|
|
13
|
+
try:
|
|
14
|
+
return Resampling[name]
|
|
15
|
+
except KeyError:
|
|
16
|
+
valid = ", ".join([r.name for r in Resampling])
|
|
17
|
+
raise ValueError(f"Invalid resampling method {value}. Valid values are: {valid}")
|
|
18
|
+
raise TypeError("resampling method must be one from rasterio.enums.Resampling or a string")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Helper function for raster auto-alignment
|
|
22
|
+
def align_raster_to_target(ds: EEORasterDataset, target: EEORasterDataset,
|
|
23
|
+
method: str = "bilinear") -> EEORasterDataset:
|
|
24
|
+
"""Resample dataset to match target raster's shape and transform"""
|
|
25
|
+
if ds.get_shape() == target.get_shape() or ds.get_transform() == target.get_transform():
|
|
26
|
+
return ds
|
|
27
|
+
return ds.resample(size=target.get_shape(), resampling_method=method)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# helper to mask nodata values from an EEORasterDataset
|
|
31
|
+
def mask_nodata(ds: EEORasterDataset, array: np.ndarray) -> np.ndarray:
|
|
32
|
+
nodata = ds.get_metadata().get("nodata", None)
|
|
33
|
+
if nodata is not None:
|
|
34
|
+
array = np.where(array == nodata, np.nan, array)
|
|
35
|
+
return array
|
eeo/core/__init__.py
ADDED