easy-eo 0.4.2__tar.gz → 0.6.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.
- easy_eo-0.6.0/PKG-INFO +459 -0
- easy_eo-0.6.0/README.md +392 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/__init__.py +4 -1
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/_optional.py +8 -5
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/_show_versions.py +4 -2
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/common.py +67 -0
- easy_eo-0.6.0/eeo/core/adapters/__init__.py +15 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/core/adapters/base.py +4 -3
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/core/adapters/rasterio.py +3 -1
- easy_eo-0.6.0/eeo/core/adapters/xarray.py +410 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/core/blockwise.py +15 -4
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/core/core.py +64 -17
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/core/core.pyi +3 -2
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/core/loader.py +34 -10
- easy_eo-0.6.0/eeo/core/remote.py +122 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/core/types.py +4 -0
- easy_eo-0.6.0/eeo/io/_dedupe.py +237 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/io/_stacking.py +1 -11
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/io/stac.py +124 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/io/xarray.py +69 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/ops/merge.py +9 -16
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/preprocessing/clip.py +4 -14
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/preprocessing/reproject.py +3 -8
- easy_eo-0.6.0/eeo/timeseries/__init__.py +13 -0
- easy_eo-0.6.0/eeo/timeseries/binning.py +395 -0
- easy_eo-0.6.0/eeo/timeseries/core.py +2343 -0
- easy_eo-0.6.0/eeo/timeseries/extract.py +157 -0
- easy_eo-0.6.0/eeo/timeseries/reducers.py +343 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/viz/__init__.py +3 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/viz/plot.py +16 -6
- easy_eo-0.6.0/eeo/viz/timeseries.py +278 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/pyproject.toml +13 -1
- easy_eo-0.4.2/PKG-INFO +0 -371
- easy_eo-0.4.2/README.md +0 -309
- easy_eo-0.4.2/eeo/core/adapters/__init__.py +0 -11
- {easy_eo-0.4.2 → easy_eo-0.6.0}/.gitignore +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/LICENSE +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/analysis/__init__.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/analysis/indices.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/analysis/stats.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/core/__init__.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/core/adapters/numpy.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/core/decorators.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/core/exceptions.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/core/plugins.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/core/streaming.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/datasets/__init__.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/datasets/_cache.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/datasets/_registry.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/datasets/_samples.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/io/__init__.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/io/_archive.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/io/_bands.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/io/_landsat.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/io/_sentinel2.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/io/products.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/ops/__init__.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/ops/algebra.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/preprocessing/__init__.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/preprocessing/masking.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/preprocessing/normalize.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/preprocessing/quality.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/preprocessing/resample.py +0 -0
- {easy_eo-0.4.2 → easy_eo-0.6.0}/eeo/py.typed +0 -0
easy_eo-0.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: easy-eo
|
|
3
|
+
Version: 0.6.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: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
42
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
43
|
+
Classifier: Typing :: Typed
|
|
44
|
+
Requires-Python: >=3.10
|
|
45
|
+
Requires-Dist: geopandas<2,>=1.1
|
|
46
|
+
Requires-Dist: matplotlib>=3.8
|
|
47
|
+
Requires-Dist: numpy<3,>=1.26
|
|
48
|
+
Requires-Dist: pandas>=2.0
|
|
49
|
+
Requires-Dist: rasterio<2,>=1.4
|
|
50
|
+
Provides-Extra: dev
|
|
51
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
52
|
+
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
53
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
54
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
55
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
56
|
+
Provides-Extra: lazy
|
|
57
|
+
Requires-Dist: dask[array]>=2024.8; extra == 'lazy'
|
|
58
|
+
Requires-Dist: rioxarray<1,>=0.17; extra == 'lazy'
|
|
59
|
+
Requires-Dist: xarray>=2024.7; extra == 'lazy'
|
|
60
|
+
Provides-Extra: stac
|
|
61
|
+
Requires-Dist: planetary-computer<2,>=1.0; extra == 'stac'
|
|
62
|
+
Requires-Dist: pystac-client<1,>=0.8; extra == 'stac'
|
|
63
|
+
Provides-Extra: xarray
|
|
64
|
+
Requires-Dist: rioxarray<1,>=0.17; extra == 'xarray'
|
|
65
|
+
Requires-Dist: xarray>=2024.7; extra == 'xarray'
|
|
66
|
+
Description-Content-Type: text/markdown
|
|
67
|
+
|
|
68
|
+
# Easy-EO
|
|
69
|
+
|
|
70
|
+
<p align="center">
|
|
71
|
+
<img src="https://raw.githubusercontent.com/Tommy-Burns/easy-eo/main/.github/assets/eeo_logo.png" alt="Easy-EO logo" width="200">
|
|
72
|
+
</p>
|
|
73
|
+
|
|
74
|
+
| | |
|
|
75
|
+
| :-- | :-- |
|
|
76
|
+
| **Install** | [![PyPI][pypi-badge]][pypi] [![conda-forge][conda-badge]][conda] [![Python versions][pyversions-badge]][pypi] [![Platforms][platforms-badge]][ci] |
|
|
77
|
+
| **Build & quality** | [![CI][ci-badge]][ci] [![Latest deps][latest-deps-badge]][latest-deps] [![Coverage][codecov-badge]][codecov] [![Ruff][ruff-badge]][ruff] [![Checked with mypy][mypy-badge]][mypy] |
|
|
78
|
+
| **Security** | [![CodeQL][codeql-badge]][codeql] [![OpenSSF Scorecard][scorecard-badge]][scorecard] |
|
|
79
|
+
| **Project** | [![Documentation][docs-badge]][docs] [![License: MIT][license-badge]][license] [![DOI][doi-badge]][doi] |
|
|
80
|
+
|
|
81
|
+
[pypi]: https://pypi.org/project/easy-eo/
|
|
82
|
+
[pypi-badge]: https://img.shields.io/pypi/v/easy-eo.svg
|
|
83
|
+
[conda]: https://anaconda.org/conda-forge/easy-eo
|
|
84
|
+
[conda-badge]: https://img.shields.io/conda/vn/conda-forge/easy-eo.svg
|
|
85
|
+
[pyversions-badge]: https://img.shields.io/pypi/pyversions/easy-eo.svg
|
|
86
|
+
[platforms-badge]: https://img.shields.io/badge/platform-linux%20%7C%20macOS%20%7C%20windows-lightgrey.svg
|
|
87
|
+
[ci]: https://github.com/Tommy-Burns/easy-eo/actions/workflows/ci.yml
|
|
88
|
+
[ci-badge]: https://github.com/Tommy-Burns/easy-eo/actions/workflows/ci.yml/badge.svg
|
|
89
|
+
[latest-deps]: https://github.com/Tommy-Burns/easy-eo/actions/workflows/latest-deps.yml
|
|
90
|
+
[latest-deps-badge]: https://github.com/Tommy-Burns/easy-eo/actions/workflows/latest-deps.yml/badge.svg
|
|
91
|
+
[codecov]: https://codecov.io/gh/Tommy-Burns/easy-eo
|
|
92
|
+
[codecov-badge]: https://codecov.io/gh/Tommy-Burns/easy-eo/branch/main/graph/badge.svg
|
|
93
|
+
[ruff]: https://github.com/astral-sh/ruff
|
|
94
|
+
[ruff-badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
|
|
95
|
+
[mypy]: https://mypy-lang.org/
|
|
96
|
+
[mypy-badge]: https://img.shields.io/badge/mypy-checked-2a6db2.svg
|
|
97
|
+
[codeql]: https://github.com/Tommy-Burns/easy-eo/actions/workflows/codeql.yml
|
|
98
|
+
[codeql-badge]: https://github.com/Tommy-Burns/easy-eo/actions/workflows/codeql.yml/badge.svg
|
|
99
|
+
[scorecard]: https://scorecard.dev/viewer/?uri=github.com/Tommy-Burns/easy-eo
|
|
100
|
+
[scorecard-badge]: https://api.scorecard.dev/projects/github.com/Tommy-Burns/easy-eo/badge
|
|
101
|
+
[docs]: https://easy-eo.readthedocs.io/en/latest/?badge=latest
|
|
102
|
+
[docs-badge]: https://readthedocs.org/projects/easy-eo/badge/?version=latest
|
|
103
|
+
[license]: https://github.com/Tommy-Burns/easy-eo/blob/main/LICENSE
|
|
104
|
+
[license-badge]: https://img.shields.io/github/license/Tommy-Burns/easy-eo
|
|
105
|
+
[doi]: https://doi.org/10.5281/zenodo.21967655
|
|
106
|
+
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.21967655.svg
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
Easy-EO is a lightweight, extensible Python library for raster-based Earth
|
|
110
|
+
Observation (EO) analysis. It covers searching and loading imagery, cloud
|
|
111
|
+
masking, band algebra and spectral indices, time series and visualization, and
|
|
112
|
+
each step is a few readable lines of Python.
|
|
113
|
+
|
|
114
|
+
## From satellite archive to NDVI map
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import eeo
|
|
118
|
+
|
|
119
|
+
results = eeo.stac_search(
|
|
120
|
+
collection="sentinel-2-l2a",
|
|
121
|
+
bbox=(11.0, 46.5, 11.2, 46.7), # area of interest, WGS 84 lon/lat
|
|
122
|
+
datetime="2023-06-01/2023-08-31",
|
|
123
|
+
cloud_cover=20,
|
|
124
|
+
limit=1,
|
|
125
|
+
)
|
|
126
|
+
scene = results[0].load(assets=["B04", "B08"]) # reads only the area of interest
|
|
127
|
+
ndvi = scene.ndvi(red="B04", nir="B08")
|
|
128
|
+
ndvi.plot_raster()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
That is the whole workflow: no scene downloads and no GDAL wrangling. The search
|
|
132
|
+
queries [Microsoft Planetary Computer](https://planetarycomputer.microsoft.com/)
|
|
133
|
+
by default, and any other STAC catalog works through `catalog="<catalog URL>"`.
|
|
134
|
+
The load streams only the window covering your bounding box over HTTP, which
|
|
135
|
+
here is 14 MB out of a 240 MB Sentinel-2 tile and takes a few seconds.
|
|
136
|
+
|
|
137
|
+
`stac_search` needs the STAC extra: `pip install "easy-eo[stac]"`, or the
|
|
138
|
+
[conda equivalent](#installation). To start offline instead, jump to the
|
|
139
|
+
[hosted sample dataset](#quick-example), which needs no network after the first
|
|
140
|
+
call.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Features
|
|
145
|
+
|
|
146
|
+
| | What you get | Guide |
|
|
147
|
+
| --- | --- | --- |
|
|
148
|
+
| **Catalog search** | `stac_search()` over any STAC catalog, by bounding box or by a polygon; loading reads only your area of interest over HTTP; `deduplicate()` drops reprocessed copies of the same acquisition | [Satellite data](https://easy-eo.readthedocs.io/en/latest/user_guide/loading_satellite_data.html) |
|
|
149
|
+
| **Downloaded products** | `load_sentinel2()` and `load_landsat()` read a Sentinel-2 L2A `.SAFE` or a Landsat Collection 2 product as you downloaded it, still zipped or tarred, with bands chosen by name and the acquisition time recorded | [Downloaded scenes](https://easy-eo.readthedocs.io/en/latest/user_guide/loading_downloaded_scenes.html) |
|
|
150
|
+
| **Cloud masking** | `mask_clouds()` sets cloud, cloud shadow and cirrus to nodata from the Sentinel-2 scene classification or the Landsat `QA_PIXEL` band, including medium-confidence cloud; `clear_fraction()` reports how much of a scene is left | [Masking clouds](https://easy-eo.readthedocs.io/en/latest/user_guide/masking_clouds.html) |
|
|
151
|
+
| **Time series** | `time_series()` builds a series from a search, a folder of GeoTIFFs or scenes you loaded; `map()` applies any operation to every date; median/mean/min/max, cloud-free `composite()`, monthly or weekly `resample_time()`; `extract_at()` returns a pixel's trajectory as a pandas DataFrame | [Time series](https://easy-eo.readthedocs.io/en/latest/user_guide/time_series.html) |
|
|
152
|
+
| **Spectral indices** | `ndvi`, `ndwi`, `ndmi`, `ndbi`, `evi`, `savi`, plus `normalized_difference` for anything else, all chainable and float32 | [Spectral indices](https://easy-eo.readthedocs.io/en/latest/user_guide/spectral_indices.html) |
|
|
153
|
+
| **Band algebra** | `add`, `subtract`, `multiply`, `divide`, `power`, `sqrt`, `log`, `absolute`, and the matching operators | [Operations](https://easy-eo.readthedocs.io/en/latest/user_guide/ops.html) |
|
|
154
|
+
| **Preprocessing** | Clip to a bounding box or a vector, resample, reproject, mosaic, stack, normalize (min-max, percentile, z-score) | [Preprocessing](https://easy-eo.readthedocs.io/en/latest/user_guide/preprocessing.html) |
|
|
155
|
+
| **Large rasters** | Algebra, indices and normalization run a block at a time; `apply_blockwise()` streams a computation of your own to disk; `load_raster(path, chunks=...)` opens a raster lazily with dask, including a COG over HTTP | [Large rasters](https://easy-eo.readthedocs.io/en/latest/user_guide/large_rasters.html) |
|
|
156
|
+
| **Named bands and metadata** | Address any band as `"red"` or `"nir"` wherever a 1-based index works; names survive a GeoTIFF round-trip; a timestamp and free-form `attrs` follow the data through every operation; `describe()` summarises a raster without reading its pixels | [Naming bands](https://easy-eo.readthedocs.io/en/latest/user_guide/band_names.html) · [The dataset](https://easy-eo.readthedocs.io/en/latest/user_guide/core_dataset.html) |
|
|
157
|
+
| **Statistics** | Per-band min/max/mean/percentile with their pixel locations, and value extraction at a coordinate | [Statistical locations](https://easy-eo.readthedocs.io/en/latest/user_guide/statistical_locations.html) |
|
|
158
|
+
| **Visualization** | Single bands, RGB composites, histograms, map-plus-histogram views, and for a series a trajectory plot and a filmstrip of dates, all read at display resolution | [Visualization](https://easy-eo.readthedocs.io/en/latest/user_guide/visualization.html) |
|
|
159
|
+
| **Predictable nodata and dtype** | One written-down contract every operation follows: mask before compute, nodata stays contagious, fractional results are float32 | [Nodata and dtype](https://easy-eo.readthedocs.io/en/latest/user_guide/nodata_and_dtype.html) |
|
|
160
|
+
| **Ecosystem interop** | `to_xarray()` / `from_xarray()` in both directions, and `to_xarray()` on a whole series; NumPy, Rasterio and xarray backends behind one interface | [xarray interop](https://easy-eo.readthedocs.io/en/latest/user_guide/xarray_interop.html) · [Backends](https://easy-eo.readthedocs.io/en/latest/backends.html) |
|
|
161
|
+
| **Typed and tested** | Ships `py.typed`, 2,800+ tests, 99% coverage, checked on Python 3.10-3.14 across Linux, macOS and Windows | [Contributing](https://github.com/Tommy-Burns/easy-eo/blob/main/CONTRIBUTING.md) |
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Before and after
|
|
166
|
+
|
|
167
|
+
One ordinary task: clip a 4-band scene to an area of interest held in a vector
|
|
168
|
+
file, compute NDVI, and save it as a GeoTIFF. Both versions below run as
|
|
169
|
+
written, against the same
|
|
170
|
+
[hosted sample dataset](https://easy-eo.readthedocs.io/en/latest/user_guide/sample_data.html)
|
|
171
|
+
(a 1024x1024 Sentinel-2 subset and a boundary polygon), so you can paste either
|
|
172
|
+
one and watch it work.
|
|
173
|
+
|
|
174
|
+
Here it is in raw Rasterio, GeoPandas and NumPy, with Easy-EO not installed at
|
|
175
|
+
all:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
import geopandas as gpd
|
|
179
|
+
import numpy as np
|
|
180
|
+
import rasterio
|
|
181
|
+
from rasterio.mask import mask
|
|
182
|
+
|
|
183
|
+
BASE = "https://github.com/Tommy-Burns/easy-eo/releases/download/sample-data-v1/"
|
|
184
|
+
|
|
185
|
+
with rasterio.open(BASE + "sentinel2_small_cog.tif") as src:
|
|
186
|
+
aoi = gpd.read_file(BASE + "roi.gpkg").to_crs(src.crs)
|
|
187
|
+
clipped, transform = mask(src, aoi.geometry.values, crop=True)
|
|
188
|
+
bands = {name: i for i, name in enumerate(src.descriptions)}
|
|
189
|
+
nodata = src.nodata
|
|
190
|
+
profile = src.profile
|
|
191
|
+
|
|
192
|
+
red = clipped[bands["red"]].astype("float32")
|
|
193
|
+
nir = clipped[bands["nir"]].astype("float32")
|
|
194
|
+
|
|
195
|
+
valid = (red != nodata) & (nir != nodata)
|
|
196
|
+
total = nir + red
|
|
197
|
+
ndvi = np.where(valid & (total != 0), (nir - red) / np.where(total == 0, 1, total), 0.0)
|
|
198
|
+
ndvi = np.where(valid, ndvi, np.nan).astype("float32")
|
|
199
|
+
|
|
200
|
+
profile.update(
|
|
201
|
+
count=1, dtype="float32", nodata=np.nan,
|
|
202
|
+
height=ndvi.shape[0], width=ndvi.shape[1], transform=transform,
|
|
203
|
+
)
|
|
204
|
+
with rasterio.open("ndvi.tif", "w", **profile) as dst:
|
|
205
|
+
dst.write(ndvi, 1)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
And in Easy-EO, where `load_sample_dataset()` fetches the same two files and
|
|
209
|
+
caches them:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
import eeo
|
|
213
|
+
from eeo.datasets import load_sample_dataset
|
|
214
|
+
|
|
215
|
+
sd = load_sample_dataset()
|
|
216
|
+
|
|
217
|
+
(
|
|
218
|
+
eeo.load_raster(path=sd.sentinel2_cog_stacked)
|
|
219
|
+
.clip_raster_with_vector(vector_file=sd.boundary)
|
|
220
|
+
.ndvi(red="red", nir="nir")
|
|
221
|
+
.save_raster(path="ndvi.tif")
|
|
222
|
+
)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Both blocks produce **byte-identical output**: the same shape, CRS, transform
|
|
226
|
+
and nodata, and every pixel value, including the roughly one quarter of the
|
|
227
|
+
image that the clip masks away.
|
|
228
|
+
|
|
229
|
+
So the point is not the line count. Rasterio makes you take four decisions by
|
|
230
|
+
hand, and each one is a chance to be quietly wrong: reprojecting the AOI into
|
|
231
|
+
the raster's CRS (the sample boundary is lon/lat, the scene is UTM), mapping
|
|
232
|
+
band names to indices, masking nodata before the arithmetic, and rebuilding the
|
|
233
|
+
output profile. Drop just the mask and NDVI comes out as `0.0` across the
|
|
234
|
+
clipped-away quarter of the image, a value that looks like bare ground in your
|
|
235
|
+
statistics and your plot rather than like missing data.
|
|
236
|
+
|
|
237
|
+
Easy-EO applies those same rules for you, consistently, on every operation.
|
|
238
|
+
They are written down in the
|
|
239
|
+
[nodata and dtype contract](https://easy-eo.readthedocs.io/en/latest/user_guide/nodata_and_dtype.html),
|
|
240
|
+
and each one is backed by tests.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## A season, not a scene
|
|
245
|
+
|
|
246
|
+
One cloud-free NDVI map per month over a growing season, and how one field
|
|
247
|
+
changed through it:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
import eeo
|
|
251
|
+
|
|
252
|
+
results = eeo.stac_search(
|
|
253
|
+
collection="sentinel-2-l2a",
|
|
254
|
+
bbox=(5.60, 52.05, 5.65, 52.085),
|
|
255
|
+
datetime="2023-04-01/2023-09-30",
|
|
256
|
+
cloud_cover=50,
|
|
257
|
+
)
|
|
258
|
+
ts = eeo.time_series(source=results.deduplicate(), assets=["B04", "B08", "SCL"])
|
|
259
|
+
|
|
260
|
+
monthly = ts.resample_time(freq="MS").composite() # one cloud-free raster per month
|
|
261
|
+
ndvi = monthly.map(eeo.ndvi, red="B04", nir="B08", name="ndvi")
|
|
262
|
+
|
|
263
|
+
ndvi.plot_filmstrip(cmap="RdYlGn") # a small map per month
|
|
264
|
+
ndvi.extract_at(coordinates=(5.625, 52.0675), crs="EPSG:4326") # a pandas DataFrame
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`composite()` masks every date with its own scene classification band before
|
|
268
|
+
taking the median, so a pixel is built only from the dates it was clear, and
|
|
269
|
+
the `SCL` band is dropped from the result. A month that was overcast
|
|
270
|
+
throughout comes back as nodata rather than as a cloud-coloured value.
|
|
271
|
+
|
|
272
|
+
`deduplicate()` keeps one copy of each acquisition. Catalogs publish
|
|
273
|
+
reprocessed scenes alongside the originals, and without it the repeated dates
|
|
274
|
+
would count twice in the median.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Products as you downloaded them
|
|
279
|
+
|
|
280
|
+
A Sentinel-2 product still zipped, and a Landsat product still tarred, read by
|
|
281
|
+
band name:
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
import eeo
|
|
285
|
+
|
|
286
|
+
s2 = eeo.load_sentinel2(
|
|
287
|
+
path="S2A_MSIL2A_20260814T100041_N0512_R122_T33TVM_20260814T151116.SAFE.zip",
|
|
288
|
+
bands=["red", "nir", "scl"],
|
|
289
|
+
)
|
|
290
|
+
l9 = eeo.load_landsat(
|
|
291
|
+
path="LC09_L2SP_192027_20260815_20260816_02_T1.tar",
|
|
292
|
+
bands=["red", "nir08", "qa_pixel"],
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
clear = l9.mask_clouds() # cloud, cloud shadow and cirrus set to nodata
|
|
296
|
+
clear.clear_fraction() # share of the scene still usable
|
|
297
|
+
ndvi = clear.ndvi(red="red", nir="nir08")
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Each loader resolves the band names to the right files for that mission and
|
|
301
|
+
product, and records the acquisition time and product metadata. Pass
|
|
302
|
+
`bbox=(minx, miny, maxx, maxy)` in lon/lat to read only part of the scene.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Installation
|
|
307
|
+
|
|
308
|
+
Python 3.10 or newer, from either package manager:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
pip install easy-eo
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
conda install -c conda-forge easy-eo
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
That is everything you need for the core: raster I/O, the product loaders,
|
|
319
|
+
cloud masking, algebra, indices, preprocessing, time series and plotting.
|
|
320
|
+
Three heavier integrations are kept separate, so you only install them if you
|
|
321
|
+
use them:
|
|
322
|
+
|
|
323
|
+
| Adds | pip | conda |
|
|
324
|
+
| --- | --- | --- |
|
|
325
|
+
| `stac_search()` and loading scenes from STAC catalogs | `pip install "easy-eo[stac]"` | `conda install -c conda-forge easy-eo pystac-client planetary-computer` |
|
|
326
|
+
| `to_xarray()` / `from_xarray()` | `pip install "easy-eo[xarray]"` | `conda install -c conda-forge easy-eo xarray rioxarray` |
|
|
327
|
+
| Opening a raster lazily (`load_raster(path, chunks=...)`), including over HTTP | `pip install "easy-eo[lazy]"` | `conda install -c conda-forge easy-eo xarray rioxarray dask-core` |
|
|
328
|
+
|
|
329
|
+
pip extras compose: `pip install "easy-eo[stac,xarray]"` installs both. conda
|
|
330
|
+
has no concept of extras, so `conda install "easy-eo[stac]"` is not a valid
|
|
331
|
+
command; the same packages are installed by name instead, as above.
|
|
332
|
+
|
|
333
|
+
**Use one package manager, not both.** If Easy-EO came from conda, install the
|
|
334
|
+
extras from conda too. conda's solver knows nothing about pip-installed files,
|
|
335
|
+
so a later `conda install` or `conda update` can overwrite them or leave a
|
|
336
|
+
second copy of a shared dependency in the environment. Every extra dependency
|
|
337
|
+
is on conda-forge, so there is no reason to mix.
|
|
338
|
+
|
|
339
|
+
Without an extra installed, the features that need it raise a
|
|
340
|
+
`MissingDependencyError` that tells you exactly what to install. Nothing fails
|
|
341
|
+
silently at import time.
|
|
342
|
+
|
|
343
|
+
## Quick example
|
|
344
|
+
|
|
345
|
+
Offline after the first call, using the hosted sample dataset:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
import eeo
|
|
349
|
+
from eeo.datasets import load_sample_dataset
|
|
350
|
+
|
|
351
|
+
sd = load_sample_dataset()
|
|
352
|
+
scene = eeo.load_raster(path=sd.sentinel2_cog_stacked) # red, green, blue, nir bands
|
|
353
|
+
scene.describe() # metadata only, no pixels read
|
|
354
|
+
|
|
355
|
+
ndvi = scene.ndvi(red="red", nir="nir", name="NDVI")
|
|
356
|
+
ndvi.plot_raster(cmap="RdYlGn", colorbar=True)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Operations chain, and a multi-line chain is wrapped in parentheses:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
result = (
|
|
363
|
+
scene.clip_raster_with_vector(vector_file=sd.boundary)
|
|
364
|
+
.ndvi(red="red", nir="nir")
|
|
365
|
+
.normalize_percentile(lower_percentile=2, upper_percentile=98)
|
|
366
|
+
)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Tutorials
|
|
370
|
+
|
|
371
|
+
Seventeen runnable notebooks live in
|
|
372
|
+
[`examples/`](https://github.com/Tommy-Burns/easy-eo/blob/main/examples/README.md),
|
|
373
|
+
from first install through to complete analyses (flood mapping, drought stress,
|
|
374
|
+
land cover, terrain, and a season of cloud-free composites). Each one opens in
|
|
375
|
+
Colab with no local setup, because the first cell installs Easy-EO when it
|
|
376
|
+
detects Colab:
|
|
377
|
+
|
|
378
|
+
| | |
|
|
379
|
+
| --- | --- |
|
|
380
|
+
| [Quickstart: NDVI](https://github.com/Tommy-Burns/easy-eo/blob/main/examples/00_getting_started/02_quickstart_ndvi.ipynb): open a scene, compute an index, plot it | [](https://colab.research.google.com/github/Tommy-Burns/easy-eo/blob/main/examples/00_getting_started/02_quickstart_ndvi.ipynb) |
|
|
381
|
+
| [Search and load from STAC](https://github.com/Tommy-Burns/easy-eo/blob/main/examples/02_data_access/02_stac_search_and_load.ipynb): find real scenes, read them over HTTP | [](https://colab.research.google.com/github/Tommy-Burns/easy-eo/blob/main/examples/02_data_access/02_stac_search_and_load.ipynb) |
|
|
382
|
+
| [Cloud-free composites and trends](https://github.com/Tommy-Burns/easy-eo/blob/main/examples/04_timeseries/01_cloud_free_composite_and_trends.ipynb): a season of Sentinel-2, masked, composited and followed through time | [](https://colab.research.google.com/github/Tommy-Burns/easy-eo/blob/main/examples/04_timeseries/01_cloud_free_composite_and_trends.ipynb) |
|
|
383
|
+
| [Flood mapping with NDWI](https://github.com/Tommy-Burns/easy-eo/blob/main/examples/03_real_world/01_flood_mapping_ndwi.ipynb): Pakistan 2022, before/after, area affected | [](https://colab.research.google.com/github/Tommy-Burns/easy-eo/blob/main/examples/03_real_world/01_flood_mapping_ndwi.ipynb) |
|
|
384
|
+
|
|
385
|
+
The full index, including what each notebook covers, is in
|
|
386
|
+
[`examples/README.md`](https://github.com/Tommy-Burns/easy-eo/blob/main/examples/README.md)
|
|
387
|
+
and on the [tutorials page](https://easy-eo.readthedocs.io/en/latest/tutorials.html)
|
|
388
|
+
of the documentation.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Gallery
|
|
393
|
+
|
|
394
|
+
Every image below comes straight out of an Easy-EO plotting call on the sample
|
|
395
|
+
dataset, with the library's own defaults and no touch-ups. Regenerate them all
|
|
396
|
+
with `python scripts/build_gallery.py`.
|
|
397
|
+
|
|
398
|
+
| | |
|
|
399
|
+
| --- | --- |
|
|
400
|
+
| <img src="https://raw.githubusercontent.com/Tommy-Burns/easy-eo/main/.github/assets/gallery/composite_true_colour.jpg" alt="True colour composite of a Sentinel-2 scene"> | <img src="https://raw.githubusercontent.com/Tommy-Burns/easy-eo/main/.github/assets/gallery/composite_false_colour.jpg" alt="False colour composite, vegetation in red"> |
|
|
401
|
+
| `scene.plot_composite(bands=["red", "green", "blue"])` | `scene.plot_composite(bands=["nir", "red", "green"])` |
|
|
402
|
+
| <img src="https://raw.githubusercontent.com/Tommy-Burns/easy-eo/main/.github/assets/gallery/index_ndvi.jpg" alt="NDVI map on a red-yellow-green colour scale"> | <img src="https://raw.githubusercontent.com/Tommy-Burns/easy-eo/main/.github/assets/gallery/dem_terrain.jpg" alt="Copernicus DEM elevation map"> |
|
|
403
|
+
| `scene.ndvi(red="red", nir="nir", name="NDVI").plot_raster(cmap="RdYlGn")` | `dem.plot_raster(cmap="Spectral_r")`, the same call on a DEM |
|
|
404
|
+
| <img src="https://raw.githubusercontent.com/Tommy-Burns/easy-eo/main/.github/assets/gallery/histogram_bands.png" alt="Value distribution of each of the four bands"> | <img src="https://raw.githubusercontent.com/Tommy-Burns/easy-eo/main/.github/assets/gallery/clip_ndvi_histogram.png" alt="NDVI clipped to a hexagonal boundary beside its histogram"> |
|
|
405
|
+
| `scene.plot_histogram()`, every band at once | `clipped.plot_raster_with_histogram(cmap="RdYlGn")` |
|
|
406
|
+
|
|
407
|
+
Bands are addressed by name throughout (`"red"`, `"nir"`) because the sample
|
|
408
|
+
carries band descriptions; a 1-based index works anywhere a name does.
|
|
409
|
+
|
|
410
|
+
## Backends
|
|
411
|
+
|
|
412
|
+
One dataset type, with three backends underneath it:
|
|
413
|
+
|
|
414
|
+
| Backend | What it is for |
|
|
415
|
+
| --- | --- |
|
|
416
|
+
| Rasterio | Files and COGs GDAL can open, local or over HTTP, from `load_raster()` |
|
|
417
|
+
| NumPy | In-memory arrays with a CRS and transform attached, from `load_array()` and the product loaders |
|
|
418
|
+
| xarray + dask | Lazy, chunked reading of rasters larger than memory, from `load_raster(path, chunks=...)` (the `lazy` extra) |
|
|
419
|
+
|
|
420
|
+
Every operation, statistic and plot works on the Rasterio and lazy backends and
|
|
421
|
+
returns the same answer on both. On the NumPy backend, clipping, reprojecting,
|
|
422
|
+
mosaicking and stacking need a `.to_rasterio()` first, and the error says so.
|
|
423
|
+
|
|
424
|
+
See [Backends](https://easy-eo.readthedocs.io/en/latest/backends.html) for how
|
|
425
|
+
they fit together.
|
|
426
|
+
|
|
427
|
+
## Documentation
|
|
428
|
+
|
|
429
|
+
Full documentation, with user guides and the API reference:
|
|
430
|
+
[easy-eo.readthedocs.io](https://easy-eo.readthedocs.io/en/latest/index.html)
|
|
431
|
+
|
|
432
|
+
## Citing Easy-EO
|
|
433
|
+
|
|
434
|
+
If Easy-EO contributes to published work, please cite it. The DOI
|
|
435
|
+
[10.5281/zenodo.21967655](https://doi.org/10.5281/zenodo.21967655) always
|
|
436
|
+
resolves to the latest release, and
|
|
437
|
+
[`CITATION.cff`](https://github.com/Tommy-Burns/easy-eo/blob/main/CITATION.cff)
|
|
438
|
+
drives GitHub's "Cite this repository" button. The
|
|
439
|
+
[citation page](https://easy-eo.readthedocs.io/en/latest/citation.html) has a
|
|
440
|
+
BibTeX entry, and a separate one for the sample dataset.
|
|
441
|
+
|
|
442
|
+
## Project status
|
|
443
|
+
|
|
444
|
+
Active development. The API is stabilising but may change before v1.0, and
|
|
445
|
+
every breaking change is listed under a **Breaking** heading in the
|
|
446
|
+
[changelog](https://github.com/Tommy-Burns/easy-eo/blob/main/CHANGELOG.md).
|
|
447
|
+
|
|
448
|
+
## Contributing
|
|
449
|
+
|
|
450
|
+
Contributions are welcome: bug reports, feature requests and documentation
|
|
451
|
+
improvements as much as code. Please open an issue or pull request on GitHub,
|
|
452
|
+
and see [`CONTRIBUTING.md`](https://github.com/Tommy-Burns/easy-eo/blob/main/CONTRIBUTING.md)
|
|
453
|
+
for how to set up a development environment and run the checks. Report
|
|
454
|
+
security issues privately, as described in
|
|
455
|
+
[`SECURITY.md`](https://github.com/Tommy-Burns/easy-eo/blob/main/SECURITY.md).
|
|
456
|
+
|
|
457
|
+
## License
|
|
458
|
+
|
|
459
|
+
MIT License © 2025 Thomas Burns Botchwey
|