easy-eo 0.2.0__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.
Files changed (46) hide show
  1. easy_eo-0.3.0/PKG-INFO +343 -0
  2. easy_eo-0.3.0/README.md +282 -0
  3. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/__init__.py +1 -1
  4. easy_eo-0.3.0/eeo/_optional.py +152 -0
  5. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/core/adapters/base.py +3 -1
  6. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/core/adapters/numpy.py +2 -1
  7. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/core/adapters/rasterio.py +3 -2
  8. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/core/core.py +8 -7
  9. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/core/core.pyi +27 -16
  10. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/core/exceptions.py +7 -3
  11. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/core/loader.py +3 -2
  12. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/core/types.py +6 -0
  13. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/ops/merge.py +3 -2
  14. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/preprocessing/clip.py +4 -3
  15. easy_eo-0.3.0/eeo/viz/plot.py +1144 -0
  16. {easy_eo-0.2.0 → easy_eo-0.3.0}/pyproject.toml +3 -1
  17. easy_eo-0.2.0/PKG-INFO +0 -176
  18. easy_eo-0.2.0/README.md +0 -117
  19. easy_eo-0.2.0/eeo/_optional.py +0 -65
  20. easy_eo-0.2.0/eeo/viz/plot.py +0 -640
  21. {easy_eo-0.2.0 → easy_eo-0.3.0}/.gitignore +0 -0
  22. {easy_eo-0.2.0 → easy_eo-0.3.0}/LICENSE +0 -0
  23. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/_show_versions.py +0 -0
  24. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/analysis/__init__.py +0 -0
  25. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/analysis/indices.py +0 -0
  26. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/analysis/stats.py +0 -0
  27. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/common.py +0 -0
  28. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/core/__init__.py +0 -0
  29. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/core/adapters/__init__.py +0 -0
  30. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/core/decorators.py +0 -0
  31. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/core/plugins.py +0 -0
  32. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/datasets/__init__.py +0 -0
  33. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/datasets/_cache.py +0 -0
  34. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/datasets/_registry.py +0 -0
  35. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/datasets/_samples.py +0 -0
  36. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/io/__init__.py +0 -0
  37. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/io/stac.py +0 -0
  38. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/io/xarray.py +0 -0
  39. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/ops/__init__.py +0 -0
  40. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/ops/algebra.py +0 -0
  41. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/preprocessing/__init__.py +0 -0
  42. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/preprocessing/normalize.py +0 -0
  43. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/preprocessing/reproject.py +0 -0
  44. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/preprocessing/resample.py +0 -0
  45. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/py.typed +0 -0
  46. {easy_eo-0.2.0 → easy_eo-0.3.0}/eeo/viz/__init__.py +0 -0
easy_eo-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,343 @@
1
+ Metadata-Version: 2.4
2
+ Name: easy-eo
3
+ Version: 0.3.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
+ Requires-Python: >=3.10
44
+ Requires-Dist: geopandas<2,>=1.1
45
+ Requires-Dist: matplotlib>=3.8
46
+ Requires-Dist: numpy<3,>=1.26
47
+ Requires-Dist: rasterio<2,>=1.4
48
+ Provides-Extra: dev
49
+ Requires-Dist: mypy>=1.10; extra == 'dev'
50
+ Requires-Dist: pre-commit>=3.7; extra == 'dev'
51
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
52
+ Requires-Dist: pytest>=8.0; extra == 'dev'
53
+ Requires-Dist: ruff>=0.6; extra == 'dev'
54
+ Provides-Extra: stac
55
+ Requires-Dist: planetary-computer<2,>=1.0; extra == 'stac'
56
+ Requires-Dist: pystac-client<1,>=0.8; extra == 'stac'
57
+ Provides-Extra: xarray
58
+ Requires-Dist: rioxarray<1,>=0.17; extra == 'xarray'
59
+ Requires-Dist: xarray>=2024.7; extra == 'xarray'
60
+ Description-Content-Type: text/markdown
61
+
62
+ # Easy-EO
63
+
64
+ <p align="center">
65
+ <img src="https://raw.githubusercontent.com/Tommy-Burns/easy-eo/main/.github/assets/eeo_logo.png" alt="Easy-EO logo" width="200">
66
+ </p>
67
+
68
+ [![CI](https://github.com/Tommy-Burns/easy-eo/actions/workflows/ci.yml/badge.svg)](https://github.com/Tommy-Burns/easy-eo/actions/workflows/ci.yml)
69
+ [![codecov](https://codecov.io/gh/Tommy-Burns/easy-eo/branch/main/graph/badge.svg)](https://codecov.io/gh/Tommy-Burns/easy-eo)
70
+ [![PyPI](https://img.shields.io/pypi/v/easy-eo.svg)](https://pypi.org/project/easy-eo/)
71
+ [![conda-forge](https://img.shields.io/conda/vn/conda-forge/easy-eo.svg)](https://anaconda.org/conda-forge/easy-eo)
72
+ [![Python versions](https://img.shields.io/pypi/pyversions/easy-eo.svg)](https://pypi.org/project/easy-eo/)
73
+ [![Documentation Status](https://readthedocs.org/projects/easy-eo/badge/?version=latest)](https://easy-eo.readthedocs.io/en/latest/?badge=latest)
74
+ [![License: MIT](https://img.shields.io/github/license/Tommy-Burns/easy-eo)](https://github.com/Tommy-Burns/easy-eo/blob/main/LICENSE)
75
+
76
+ Easy-EO is a lightweight, extensible Python library for raster-based Earth
77
+ Observation (EO) analysis: chainable raster processing, band algebra, spectral indices,
78
+ and visualization, in a few readable lines instead of dealing with complex boilerplate code.
79
+
80
+ ## From satellite archive to NDVI map
81
+
82
+ ```python
83
+ import eeo
84
+
85
+ results = eeo.stac_search(
86
+ "sentinel-2-l2a",
87
+ bbox=(11.0, 46.5, 11.2, 46.7), # area of interest, WGS 84 lon/lat
88
+ datetime="2023-06-01/2023-08-31",
89
+ cloud_cover=20,
90
+ limit=1,
91
+ )
92
+ scene = results[0].load(["B04", "B08"]) # reads only the area of interest
93
+ ndvi = scene.ndvi(red="B04", nir="B08")
94
+ ndvi.plot_raster()
95
+ ```
96
+
97
+ That is the whole workflow - no scene downloads, no GDAL wrangling. The search
98
+ queries [Microsoft Planetary Computer](https://planetarycomputer.microsoft.com/)
99
+ (any STAC catalog works, you have to pass `catalog="your stac catalog"`), and the load streams just the window covering your
100
+ bounding box over HTTP: 14 MB out of a 240 MB Sentinel-2 tile, in a few seconds.
101
+
102
+ The `stac_search` needs the STAC extra - `pip install "easy-eo[stac]"`, or the [conda equivalent](#installation). If you prefer to start offline, jump to the [hosted sample dataset](#quick-example), which needs no network after the first call.
103
+
104
+ ---
105
+
106
+ ## Features
107
+
108
+ | | What you get | Guide |
109
+ | --- | --- | --- |
110
+ | **Data access** | `stac_search()` over any STAC catalog, loading only your area of interest over HTTP; GeoTIFF/COG and anything else GDAL reads; a hosted sample dataset one call away | [Satellite data](https://easy-eo.readthedocs.io/en/latest/user_guide/loading_satellite_data.html) · [Sample data](https://easy-eo.readthedocs.io/en/latest/user_guide/sample_data.html) |
111
+ | **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) |
112
+ | **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) |
113
+ | **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) |
114
+ | **Named bands** | Address any band as `"red"` or `"nir"` wherever a 1-based index works; names survive a GeoTIFF round-trip | [Naming bands](https://easy-eo.readthedocs.io/en/latest/user_guide/band_names.html) |
115
+ | **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) |
116
+ | **Visualization** | Single bands, RGB composites, histograms, and map-plus-histogram views, read at display resolution | [Visualization](https://easy-eo.readthedocs.io/en/latest/user_guide/visualization.html) |
117
+ | **Predictable nodata & dtype** | One written-down contract every operation follows: mask before compute, nodata stays contagious, fractional results are float32 | [Nodata & dtype](https://easy-eo.readthedocs.io/en/latest/user_guide/nodata_and_dtype.html) |
118
+ | **Ecosystem interop** | `to_xarray()` / `from_xarray()` in both directions; NumPy and Rasterio 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) |
119
+ | **Typed and tested** | Ships `py.typed`, 950+ tests, ~95% 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) |
120
+
121
+ ---
122
+
123
+ ## Before and after
124
+
125
+ One ordinary task: clip a 4-band scene to an area of interest held in a vector
126
+ file, compute NDVI, save it as a GeoTIFF. Both versions below run as written,
127
+ against the same [hosted sample dataset](https://easy-eo.readthedocs.io/en/latest/user_guide/sample_data.html)
128
+ - a 1024x1024 Sentinel-2 subset and a boundary polygon - so you can paste
129
+ either one and watch it work.
130
+
131
+ Here it is in raw Rasterio, GeoPandas and NumPy, with Easy-EO not installed at
132
+ all -
133
+
134
+ ```python
135
+ import geopandas as gpd
136
+ import numpy as np
137
+ import rasterio
138
+ from rasterio.mask import mask
139
+
140
+ BASE = "https://github.com/Tommy-Burns/easy-eo/releases/download/sample-data-v1/"
141
+
142
+ with rasterio.open(BASE + "sentinel2_small_cog.tif") as src:
143
+ aoi = gpd.read_file(BASE + "roi.gpkg").to_crs(src.crs)
144
+ clipped, transform = mask(src, aoi.geometry.values, crop=True)
145
+ bands = {name: i for i, name in enumerate(src.descriptions)}
146
+ nodata = src.nodata
147
+ profile = src.profile
148
+
149
+ red = clipped[bands["red"]].astype("float32")
150
+ nir = clipped[bands["nir"]].astype("float32")
151
+
152
+ valid = (red != nodata) & (nir != nodata)
153
+ total = nir + red
154
+ ndvi = np.where(valid & (total != 0), (nir - red) / np.where(total == 0, 1, total), 0.0)
155
+ ndvi = np.where(valid, ndvi, np.nan).astype("float32")
156
+
157
+ profile.update(
158
+ count=1, dtype="float32", nodata=np.nan,
159
+ height=ndvi.shape[0], width=ndvi.shape[1], transform=transform,
160
+ )
161
+ with rasterio.open("ndvi.tif", "w", **profile) as dst:
162
+ dst.write(ndvi, 1)
163
+ ```
164
+
165
+ -- and in Easy-EO, where `load_sample_dataset()` fetches the same two files and
166
+ caches them:
167
+
168
+ ```python
169
+ import eeo
170
+ from eeo.datasets import load_sample_dataset
171
+
172
+ sd = load_sample_dataset()
173
+
174
+ (
175
+ eeo.load_raster(sd.sentinel2_cog_stacked)
176
+ .clip_raster_with_vector(sd.boundary)
177
+ .ndvi(red="red", nir="nir")
178
+ .save_raster("ndvi.tif")
179
+ )
180
+ ```
181
+
182
+ Both blocks produce **byte-identical output** - same shape, CRS, transform,
183
+ nodata, and every one of the pixel values, including all pixels the clip
184
+ masks away (approx. a quarter of the image).
185
+ So the point is not the line count. It is that Rasterio makes you take four decisions by hand,
186
+ each one a chance to be quietly wrong: reprojecting the AOI into the raster's CRS
187
+ (the sample boundary is lon/lat, the scene is UTM), mapping band names to indices,
188
+ masking nodata before the arithmetic, and rebuilding the output profile. Drop just the mask and NDVI comes out as `0.0` across the clipped-away quarter of the image - a value that looks like bare
189
+ ground in your statistics and your plot, not like missing data.
190
+
191
+ Easy-EO applies those same rules for you, consistently, on every operation. They
192
+ are written down in the [nodata and dtype contract](https://easy-eo.readthedocs.io/en/latest/user_guide/nodata_and_dtype.html)
193
+ and each one is backed by tests.
194
+
195
+ ---
196
+
197
+ ## What's next
198
+
199
+ Easy-EO is built around one scene at a time, and everything above works that
200
+ way today. These are the next capabilities, in the order they are being built:
201
+
202
+ | Coming | What it unlocks |
203
+ | --- | --- |
204
+ | **Block-wise execution** | Pixel-wise operations stream window by window instead of holding whole arrays, so a chain runs in a bounded memory footprint. Today, loading is read-free and clipping is windowed, but an operation like `ndvi()` materialises the bands it touches. |
205
+ | **Lazy backend** (`easy-eo[lazy]`) | An xarray/dask-backed adapter behind the existing interface: chains on rasters larger than RAM, and COGs read straight over HTTP, with no change to your code beyond the loader call. |
206
+ | **Time series** (`EEOTimeSeries`) | Multi-date stacks as a first-class object - map any existing operation across timesteps, reduce to cloud-free median composites, pull per-pixel trajectories. STAC search results are already ordered and timestamped, ready to become one. |
207
+ | **Citable releases** | A JOSS paper and Zenodo DOI, so the library can be cited in published work. |
208
+
209
+ **Already using xarray?** You do not have to choose. `to_xarray()` and `from_xarray()` convert in both directions, so you can clip and compute indices
210
+ here, hand the result to dask or anything else in the xarray ecosystem, and come back - which is also how to work past a single machine's memory today.
211
+ See the [xarray interop guide](https://easy-eo.readthedocs.io/en/latest/user_guide/xarray_interop.html).
212
+
213
+ ---
214
+
215
+ ## Installation
216
+
217
+ Python 3.10 or newer, from either package manager:
218
+
219
+ ```bash
220
+ pip install easy-eo
221
+ ```
222
+
223
+ ```bash
224
+ conda install -c conda-forge easy-eo
225
+ ```
226
+
227
+ That is everything you need for the core: raster I/O, algebra, indices,
228
+ preprocessing and plotting. Two heavier integrations are kept separate, so you
229
+ only install them if you use them:
230
+
231
+ | Adds | pip | conda |
232
+ | --- | --- | --- |
233
+ | `stac_search()` and loading scenes from STAC catalogs | `pip install "easy-eo[stac]"` | `conda install -c conda-forge easy-eo pystac-client planetary-computer` |
234
+ | `to_xarray()` / `from_xarray()` | `pip install "easy-eo[xarray]"` | `conda install -c conda-forge easy-eo xarray rioxarray` |
235
+
236
+ pip extras compose - `pip install "easy-eo[stac,xarray]"` installs both. conda
237
+ has no concept of extras, so `conda install "easy-eo[stac]"` is not a valid
238
+ command; the same packages are simply installed by name, as above.
239
+
240
+ **Use one package manager, not both.** If Easy-EO came from conda, install the
241
+ extras from conda too. conda's solver knows nothing about pip-installed files,
242
+ so a later `conda install` or `conda update` can overwrite them or leave a
243
+ second copy of a shared dependency in the environment. Every extra dependency
244
+ is on conda-forge, so there is no reason to mix.
245
+
246
+ Without an extra installed, the features that need it raise a
247
+ `MissingDependencyError` telling you exactly what to install - nothing fails
248
+ silently at import time.
249
+
250
+ ## Quick Example
251
+ ```python
252
+ from eeo import load_raster
253
+
254
+ ds_nir = load_raster("path/to/nir.tif")
255
+ ds_red = load_raster("path/to/red.tif")
256
+
257
+ # Chainable example: clip -> resample -> compute NDVI -> multiply
258
+ result = (
259
+ ds_nir.clip_raster_with_bbox((0, 0, 1000, 1000))
260
+ .resample(scale_factor=2)
261
+ .normalized_difference(ds_red)
262
+ .multiply(100)
263
+ )
264
+ ```
265
+
266
+ ***Or try with a hosted sample data***
267
+ ```python
268
+ from eeo.datasets import load_sample_dataset
269
+ from eeo import load_raster
270
+
271
+ sd = load_sample_dataset()
272
+
273
+ scene = load_raster(sd.sentinel2_cog_stacked) # red, green, blue, nir bands
274
+
275
+ ndvi = scene.ndvi(red="red", nir="nir")
276
+ ndvi.plot_raster()
277
+ ```
278
+
279
+ ## Tutorials
280
+
281
+ Sixteen runnable notebooks live in [`examples/`](https://github.com/Tommy-Burns/easy-eo/blob/main/examples/README.md), from first
282
+ install through to complete analyses (flood mapping, drought stress, land cover,
283
+ terrain). Each one opens in Colab with no local setup — the first cell installs
284
+ Easy-EO when it detects Colab:
285
+
286
+ | | |
287
+ | --- | --- |
288
+ | [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 | [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Tommy-Burns/easy-eo/blob/main/examples/00_getting_started/02_quickstart_ndvi.ipynb) |
289
+ | [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 | [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Tommy-Burns/easy-eo/blob/main/examples/02_data_access/02_stac_search_and_load.ipynb) |
290
+ | [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 | [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Tommy-Burns/easy-eo/blob/main/examples/03_real_world/01_flood_mapping_ndwi.ipynb) |
291
+
292
+ The full index, including what each notebook covers, is in
293
+ [`examples/README.md`](https://github.com/Tommy-Burns/easy-eo/blob/main/examples/README.md) and in the
294
+ [tutorials page](https://easy-eo.readthedocs.io/en/latest/tutorials.html) of the
295
+ documentation.
296
+
297
+ ---
298
+
299
+ ## Gallery
300
+
301
+ Every image below is straight out of an Easy-EO plotting call on the sample
302
+ dataset, with the library's own defaults - no touch-ups. Regenerate them all
303
+ with `python scripts/build_gallery.py`.
304
+
305
+ | | |
306
+ | --- | --- |
307
+ | <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"> |
308
+ | `scene.plot_composite(["red", "green", "blue"])` | `scene.plot_composite(["nir", "red", "green"])` |
309
+ | <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"> |
310
+ | `scene.ndvi(red="red", nir="nir", name="NDVI").plot_raster(cmap="RdYlGn")` | `dem.plot_raster(cmap="Spectral_r")` - the same call on a DEM |
311
+ | <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"> |
312
+ | `scene.plot_histogram()` - every band at once | `clipped.plot_raster_with_histogram(cmap="RdYlGn")` |
313
+
314
+ Bands are addressed by name throughout (`"red"`, `"nir"`) because the sample
315
+ carries band descriptions; a 1-based index works anywhere a name does.
316
+
317
+ ## Supported Backends
318
+ | Backend | Description |
319
+ |----------|------------------------------------------------------|
320
+ | NumPy | Fast, in-memory arrays without I/O |
321
+ | Rasterio | Full geospatial support (CRS, transform, resampling) |
322
+
323
+
324
+ ## Documentation
325
+
326
+ 📚 Full documentation is available at:
327
+
328
+ 👉 [Easy-EO Documentation](https://easy-eo.readthedocs.io/en/latest/index.html)
329
+
330
+ ## Project Status
331
+ 🚧 Active development
332
+ The API is stabilizing but may change before v1.0.
333
+
334
+ ## Contributing
335
+ Contributions are welcome!
336
+ - Bug reports
337
+ - Feature requests
338
+ - Documentation improvements
339
+
340
+ Please open an issue or pull request on GitHub.
341
+
342
+ ## License
343
+ MIT License © 2025 Thomas Burns Botchwey
@@ -0,0 +1,282 @@
1
+ # Easy-EO
2
+
3
+ <p align="center">
4
+ <img src="https://raw.githubusercontent.com/Tommy-Burns/easy-eo/main/.github/assets/eeo_logo.png" alt="Easy-EO logo" width="200">
5
+ </p>
6
+
7
+ [![CI](https://github.com/Tommy-Burns/easy-eo/actions/workflows/ci.yml/badge.svg)](https://github.com/Tommy-Burns/easy-eo/actions/workflows/ci.yml)
8
+ [![codecov](https://codecov.io/gh/Tommy-Burns/easy-eo/branch/main/graph/badge.svg)](https://codecov.io/gh/Tommy-Burns/easy-eo)
9
+ [![PyPI](https://img.shields.io/pypi/v/easy-eo.svg)](https://pypi.org/project/easy-eo/)
10
+ [![conda-forge](https://img.shields.io/conda/vn/conda-forge/easy-eo.svg)](https://anaconda.org/conda-forge/easy-eo)
11
+ [![Python versions](https://img.shields.io/pypi/pyversions/easy-eo.svg)](https://pypi.org/project/easy-eo/)
12
+ [![Documentation Status](https://readthedocs.org/projects/easy-eo/badge/?version=latest)](https://easy-eo.readthedocs.io/en/latest/?badge=latest)
13
+ [![License: MIT](https://img.shields.io/github/license/Tommy-Burns/easy-eo)](https://github.com/Tommy-Burns/easy-eo/blob/main/LICENSE)
14
+
15
+ Easy-EO is a lightweight, extensible Python library for raster-based Earth
16
+ Observation (EO) analysis: chainable raster processing, band algebra, spectral indices,
17
+ and visualization, in a few readable lines instead of dealing with complex boilerplate code.
18
+
19
+ ## From satellite archive to NDVI map
20
+
21
+ ```python
22
+ import eeo
23
+
24
+ results = eeo.stac_search(
25
+ "sentinel-2-l2a",
26
+ bbox=(11.0, 46.5, 11.2, 46.7), # area of interest, WGS 84 lon/lat
27
+ datetime="2023-06-01/2023-08-31",
28
+ cloud_cover=20,
29
+ limit=1,
30
+ )
31
+ scene = results[0].load(["B04", "B08"]) # reads only the area of interest
32
+ ndvi = scene.ndvi(red="B04", nir="B08")
33
+ ndvi.plot_raster()
34
+ ```
35
+
36
+ That is the whole workflow - no scene downloads, no GDAL wrangling. The search
37
+ queries [Microsoft Planetary Computer](https://planetarycomputer.microsoft.com/)
38
+ (any STAC catalog works, you have to pass `catalog="your stac catalog"`), and the load streams just the window covering your
39
+ bounding box over HTTP: 14 MB out of a 240 MB Sentinel-2 tile, in a few seconds.
40
+
41
+ The `stac_search` needs the STAC extra - `pip install "easy-eo[stac]"`, or the [conda equivalent](#installation). If you prefer to start offline, jump to the [hosted sample dataset](#quick-example), which needs no network after the first call.
42
+
43
+ ---
44
+
45
+ ## Features
46
+
47
+ | | What you get | Guide |
48
+ | --- | --- | --- |
49
+ | **Data access** | `stac_search()` over any STAC catalog, loading only your area of interest over HTTP; GeoTIFF/COG and anything else GDAL reads; a hosted sample dataset one call away | [Satellite data](https://easy-eo.readthedocs.io/en/latest/user_guide/loading_satellite_data.html) · [Sample data](https://easy-eo.readthedocs.io/en/latest/user_guide/sample_data.html) |
50
+ | **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) |
51
+ | **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) |
52
+ | **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) |
53
+ | **Named bands** | Address any band as `"red"` or `"nir"` wherever a 1-based index works; names survive a GeoTIFF round-trip | [Naming bands](https://easy-eo.readthedocs.io/en/latest/user_guide/band_names.html) |
54
+ | **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) |
55
+ | **Visualization** | Single bands, RGB composites, histograms, and map-plus-histogram views, read at display resolution | [Visualization](https://easy-eo.readthedocs.io/en/latest/user_guide/visualization.html) |
56
+ | **Predictable nodata & dtype** | One written-down contract every operation follows: mask before compute, nodata stays contagious, fractional results are float32 | [Nodata & dtype](https://easy-eo.readthedocs.io/en/latest/user_guide/nodata_and_dtype.html) |
57
+ | **Ecosystem interop** | `to_xarray()` / `from_xarray()` in both directions; NumPy and Rasterio 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) |
58
+ | **Typed and tested** | Ships `py.typed`, 950+ tests, ~95% 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) |
59
+
60
+ ---
61
+
62
+ ## Before and after
63
+
64
+ One ordinary task: clip a 4-band scene to an area of interest held in a vector
65
+ file, compute NDVI, save it as a GeoTIFF. Both versions below run as written,
66
+ against the same [hosted sample dataset](https://easy-eo.readthedocs.io/en/latest/user_guide/sample_data.html)
67
+ - a 1024x1024 Sentinel-2 subset and a boundary polygon - so you can paste
68
+ either one and watch it work.
69
+
70
+ Here it is in raw Rasterio, GeoPandas and NumPy, with Easy-EO not installed at
71
+ all -
72
+
73
+ ```python
74
+ import geopandas as gpd
75
+ import numpy as np
76
+ import rasterio
77
+ from rasterio.mask import mask
78
+
79
+ BASE = "https://github.com/Tommy-Burns/easy-eo/releases/download/sample-data-v1/"
80
+
81
+ with rasterio.open(BASE + "sentinel2_small_cog.tif") as src:
82
+ aoi = gpd.read_file(BASE + "roi.gpkg").to_crs(src.crs)
83
+ clipped, transform = mask(src, aoi.geometry.values, crop=True)
84
+ bands = {name: i for i, name in enumerate(src.descriptions)}
85
+ nodata = src.nodata
86
+ profile = src.profile
87
+
88
+ red = clipped[bands["red"]].astype("float32")
89
+ nir = clipped[bands["nir"]].astype("float32")
90
+
91
+ valid = (red != nodata) & (nir != nodata)
92
+ total = nir + red
93
+ ndvi = np.where(valid & (total != 0), (nir - red) / np.where(total == 0, 1, total), 0.0)
94
+ ndvi = np.where(valid, ndvi, np.nan).astype("float32")
95
+
96
+ profile.update(
97
+ count=1, dtype="float32", nodata=np.nan,
98
+ height=ndvi.shape[0], width=ndvi.shape[1], transform=transform,
99
+ )
100
+ with rasterio.open("ndvi.tif", "w", **profile) as dst:
101
+ dst.write(ndvi, 1)
102
+ ```
103
+
104
+ -- and in Easy-EO, where `load_sample_dataset()` fetches the same two files and
105
+ caches them:
106
+
107
+ ```python
108
+ import eeo
109
+ from eeo.datasets import load_sample_dataset
110
+
111
+ sd = load_sample_dataset()
112
+
113
+ (
114
+ eeo.load_raster(sd.sentinel2_cog_stacked)
115
+ .clip_raster_with_vector(sd.boundary)
116
+ .ndvi(red="red", nir="nir")
117
+ .save_raster("ndvi.tif")
118
+ )
119
+ ```
120
+
121
+ Both blocks produce **byte-identical output** - same shape, CRS, transform,
122
+ nodata, and every one of the pixel values, including all pixels the clip
123
+ masks away (approx. a quarter of the image).
124
+ So the point is not the line count. It is that Rasterio makes you take four decisions by hand,
125
+ each one a chance to be quietly wrong: reprojecting the AOI into the raster's CRS
126
+ (the sample boundary is lon/lat, the scene is UTM), mapping band names to indices,
127
+ masking nodata before the arithmetic, and rebuilding the output profile. Drop just the mask and NDVI comes out as `0.0` across the clipped-away quarter of the image - a value that looks like bare
128
+ ground in your statistics and your plot, not like missing data.
129
+
130
+ Easy-EO applies those same rules for you, consistently, on every operation. They
131
+ are written down in the [nodata and dtype contract](https://easy-eo.readthedocs.io/en/latest/user_guide/nodata_and_dtype.html)
132
+ and each one is backed by tests.
133
+
134
+ ---
135
+
136
+ ## What's next
137
+
138
+ Easy-EO is built around one scene at a time, and everything above works that
139
+ way today. These are the next capabilities, in the order they are being built:
140
+
141
+ | Coming | What it unlocks |
142
+ | --- | --- |
143
+ | **Block-wise execution** | Pixel-wise operations stream window by window instead of holding whole arrays, so a chain runs in a bounded memory footprint. Today, loading is read-free and clipping is windowed, but an operation like `ndvi()` materialises the bands it touches. |
144
+ | **Lazy backend** (`easy-eo[lazy]`) | An xarray/dask-backed adapter behind the existing interface: chains on rasters larger than RAM, and COGs read straight over HTTP, with no change to your code beyond the loader call. |
145
+ | **Time series** (`EEOTimeSeries`) | Multi-date stacks as a first-class object - map any existing operation across timesteps, reduce to cloud-free median composites, pull per-pixel trajectories. STAC search results are already ordered and timestamped, ready to become one. |
146
+ | **Citable releases** | A JOSS paper and Zenodo DOI, so the library can be cited in published work. |
147
+
148
+ **Already using xarray?** You do not have to choose. `to_xarray()` and `from_xarray()` convert in both directions, so you can clip and compute indices
149
+ here, hand the result to dask or anything else in the xarray ecosystem, and come back - which is also how to work past a single machine's memory today.
150
+ See the [xarray interop guide](https://easy-eo.readthedocs.io/en/latest/user_guide/xarray_interop.html).
151
+
152
+ ---
153
+
154
+ ## Installation
155
+
156
+ Python 3.10 or newer, from either package manager:
157
+
158
+ ```bash
159
+ pip install easy-eo
160
+ ```
161
+
162
+ ```bash
163
+ conda install -c conda-forge easy-eo
164
+ ```
165
+
166
+ That is everything you need for the core: raster I/O, algebra, indices,
167
+ preprocessing and plotting. Two heavier integrations are kept separate, so you
168
+ only install them if you use them:
169
+
170
+ | Adds | pip | conda |
171
+ | --- | --- | --- |
172
+ | `stac_search()` and loading scenes from STAC catalogs | `pip install "easy-eo[stac]"` | `conda install -c conda-forge easy-eo pystac-client planetary-computer` |
173
+ | `to_xarray()` / `from_xarray()` | `pip install "easy-eo[xarray]"` | `conda install -c conda-forge easy-eo xarray rioxarray` |
174
+
175
+ pip extras compose - `pip install "easy-eo[stac,xarray]"` installs both. conda
176
+ has no concept of extras, so `conda install "easy-eo[stac]"` is not a valid
177
+ command; the same packages are simply installed by name, as above.
178
+
179
+ **Use one package manager, not both.** If Easy-EO came from conda, install the
180
+ extras from conda too. conda's solver knows nothing about pip-installed files,
181
+ so a later `conda install` or `conda update` can overwrite them or leave a
182
+ second copy of a shared dependency in the environment. Every extra dependency
183
+ is on conda-forge, so there is no reason to mix.
184
+
185
+ Without an extra installed, the features that need it raise a
186
+ `MissingDependencyError` telling you exactly what to install - nothing fails
187
+ silently at import time.
188
+
189
+ ## Quick Example
190
+ ```python
191
+ from eeo import load_raster
192
+
193
+ ds_nir = load_raster("path/to/nir.tif")
194
+ ds_red = load_raster("path/to/red.tif")
195
+
196
+ # Chainable example: clip -> resample -> compute NDVI -> multiply
197
+ result = (
198
+ ds_nir.clip_raster_with_bbox((0, 0, 1000, 1000))
199
+ .resample(scale_factor=2)
200
+ .normalized_difference(ds_red)
201
+ .multiply(100)
202
+ )
203
+ ```
204
+
205
+ ***Or try with a hosted sample data***
206
+ ```python
207
+ from eeo.datasets import load_sample_dataset
208
+ from eeo import load_raster
209
+
210
+ sd = load_sample_dataset()
211
+
212
+ scene = load_raster(sd.sentinel2_cog_stacked) # red, green, blue, nir bands
213
+
214
+ ndvi = scene.ndvi(red="red", nir="nir")
215
+ ndvi.plot_raster()
216
+ ```
217
+
218
+ ## Tutorials
219
+
220
+ Sixteen runnable notebooks live in [`examples/`](https://github.com/Tommy-Burns/easy-eo/blob/main/examples/README.md), from first
221
+ install through to complete analyses (flood mapping, drought stress, land cover,
222
+ terrain). Each one opens in Colab with no local setup — the first cell installs
223
+ Easy-EO when it detects Colab:
224
+
225
+ | | |
226
+ | --- | --- |
227
+ | [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 | [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Tommy-Burns/easy-eo/blob/main/examples/00_getting_started/02_quickstart_ndvi.ipynb) |
228
+ | [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 | [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Tommy-Burns/easy-eo/blob/main/examples/02_data_access/02_stac_search_and_load.ipynb) |
229
+ | [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 | [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Tommy-Burns/easy-eo/blob/main/examples/03_real_world/01_flood_mapping_ndwi.ipynb) |
230
+
231
+ The full index, including what each notebook covers, is in
232
+ [`examples/README.md`](https://github.com/Tommy-Burns/easy-eo/blob/main/examples/README.md) and in the
233
+ [tutorials page](https://easy-eo.readthedocs.io/en/latest/tutorials.html) of the
234
+ documentation.
235
+
236
+ ---
237
+
238
+ ## Gallery
239
+
240
+ Every image below is straight out of an Easy-EO plotting call on the sample
241
+ dataset, with the library's own defaults - no touch-ups. Regenerate them all
242
+ with `python scripts/build_gallery.py`.
243
+
244
+ | | |
245
+ | --- | --- |
246
+ | <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"> |
247
+ | `scene.plot_composite(["red", "green", "blue"])` | `scene.plot_composite(["nir", "red", "green"])` |
248
+ | <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"> |
249
+ | `scene.ndvi(red="red", nir="nir", name="NDVI").plot_raster(cmap="RdYlGn")` | `dem.plot_raster(cmap="Spectral_r")` - the same call on a DEM |
250
+ | <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"> |
251
+ | `scene.plot_histogram()` - every band at once | `clipped.plot_raster_with_histogram(cmap="RdYlGn")` |
252
+
253
+ Bands are addressed by name throughout (`"red"`, `"nir"`) because the sample
254
+ carries band descriptions; a 1-based index works anywhere a name does.
255
+
256
+ ## Supported Backends
257
+ | Backend | Description |
258
+ |----------|------------------------------------------------------|
259
+ | NumPy | Fast, in-memory arrays without I/O |
260
+ | Rasterio | Full geospatial support (CRS, transform, resampling) |
261
+
262
+
263
+ ## Documentation
264
+
265
+ 📚 Full documentation is available at:
266
+
267
+ 👉 [Easy-EO Documentation](https://easy-eo.readthedocs.io/en/latest/index.html)
268
+
269
+ ## Project Status
270
+ 🚧 Active development
271
+ The API is stabilizing but may change before v1.0.
272
+
273
+ ## Contributing
274
+ Contributions are welcome!
275
+ - Bug reports
276
+ - Feature requests
277
+ - Documentation improvements
278
+
279
+ Please open an issue or pull request on GitHub.
280
+
281
+ ## License
282
+ MIT License © 2025 Thomas Burns Botchwey
@@ -38,4 +38,4 @@ __all__ = [
38
38
  "MissingDependencyError",
39
39
  ]
40
40
 
41
- __version__ = "0.2.0"
41
+ __version__ = "0.3.0"