easy-eo 0.4.0__tar.gz → 0.4.2__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.4.0 → easy_eo-0.4.2}/PKG-INFO +1 -1
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/__init__.py +1 -1
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/analysis/indices.py +39 -103
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/analysis/stats.py +107 -63
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/common.py +73 -52
- easy_eo-0.4.2/eeo/core/adapters/rasterio.py +251 -0
- easy_eo-0.4.2/eeo/core/blockwise.py +408 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/core/core.py +3 -3
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/core/core.pyi +13 -0
- easy_eo-0.4.2/eeo/core/streaming.py +304 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/io/_sentinel2.py +51 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/io/products.py +8 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/io/stac.py +79 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/ops/algebra.py +74 -140
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/ops/merge.py +7 -13
- easy_eo-0.4.2/eeo/preprocessing/__init__.py +59 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/preprocessing/clip.py +11 -12
- easy_eo-0.4.2/eeo/preprocessing/masking.py +382 -0
- easy_eo-0.4.2/eeo/preprocessing/normalize.py +205 -0
- easy_eo-0.4.2/eeo/preprocessing/quality.py +956 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/preprocessing/reproject.py +17 -17
- {easy_eo-0.4.0 → easy_eo-0.4.2}/pyproject.toml +2 -1
- easy_eo-0.4.0/eeo/core/adapters/rasterio.py +0 -147
- easy_eo-0.4.0/eeo/preprocessing/__init__.py +0 -16
- easy_eo-0.4.0/eeo/preprocessing/normalize.py +0 -161
- {easy_eo-0.4.0 → easy_eo-0.4.2}/.gitignore +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/LICENSE +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/README.md +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/_optional.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/_show_versions.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/analysis/__init__.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/core/__init__.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/core/adapters/__init__.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/core/adapters/base.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/core/adapters/numpy.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/core/decorators.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/core/exceptions.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/core/loader.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/core/plugins.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/core/types.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/datasets/__init__.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/datasets/_cache.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/datasets/_registry.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/datasets/_samples.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/io/__init__.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/io/_archive.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/io/_bands.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/io/_landsat.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/io/_stacking.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/io/xarray.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/ops/__init__.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/preprocessing/resample.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/py.typed +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/viz/__init__.py +0 -0
- {easy_eo-0.4.0 → easy_eo-0.4.2}/eeo/viz/plot.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: easy-eo
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: A lightweight Earth Observation utilities library for chainable raster operations
|
|
5
5
|
Project-URL: Homepage, https://github.com/tommy-burns/easy-eo
|
|
6
6
|
Project-URL: Repository, https://github.com/tommy-burns/easy-eo
|
|
@@ -27,12 +27,8 @@ here: any normalized-difference index can be expressed with it directly.
|
|
|
27
27
|
import numpy as np
|
|
28
28
|
import rasterio as rio
|
|
29
29
|
|
|
30
|
-
from eeo.common import
|
|
31
|
-
|
|
32
|
-
apply_nodata_contract,
|
|
33
|
-
get_nodata,
|
|
34
|
-
resolve_band_index,
|
|
35
|
-
)
|
|
30
|
+
from eeo.common import align_raster_to_target, resolve_band_index
|
|
31
|
+
from eeo.core.blockwise import BlockSource, apply_blockwise
|
|
36
32
|
from eeo.core.core import EEORasterDataset
|
|
37
33
|
from eeo.core.decorators import eeo_raster_op
|
|
38
34
|
from eeo.core.exceptions import AlignmentError, ValidationError
|
|
@@ -57,15 +53,15 @@ def _safe_ratio(numerator, denominator):
|
|
|
57
53
|
return np.where(denominator != 0, quotient, np.float32(0)).astype(rio.float32)
|
|
58
54
|
|
|
59
55
|
|
|
60
|
-
def _resolve_band(ds, spec, *, auto_align, method):
|
|
61
|
-
"""Resolve a band spec to
|
|
56
|
+
def _resolve_band(ds, spec, *, auto_align, method) -> BlockSource:
|
|
57
|
+
"""Resolve a band spec to the :class:`BlockSource` that reads it.
|
|
62
58
|
|
|
63
59
|
``spec`` is a 1-based ``int`` band index into ``ds``, the ``str`` name of
|
|
64
60
|
one of ``ds``'s bands, or a separate ``EEORasterDataset`` (its first band
|
|
65
61
|
is used, aligned onto ``ds``'s grid when ``auto_align`` is True). Index and
|
|
66
62
|
name specs both go through the shared resolver, so a string is always a
|
|
67
|
-
name and never an index.
|
|
68
|
-
mask compares against the declared sentinel exactly.
|
|
63
|
+
name and never an index. The source reads the band in its own dtype, so
|
|
64
|
+
the nodata mask compares against the declared sentinel exactly.
|
|
69
65
|
"""
|
|
70
66
|
if isinstance(spec, EEORasterDataset):
|
|
71
67
|
other = spec
|
|
@@ -76,56 +72,35 @@ def _resolve_band(ds, spec, *, auto_align, method):
|
|
|
76
72
|
raise AlignmentError(
|
|
77
73
|
_ALIGN_MISMATCH.format(other=other.get_shape(), ds=ds.get_shape())
|
|
78
74
|
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"band must be an EEORasterDataset, a 1-based int band index, or a "
|
|
87
|
-
f"band name; got {type(spec).__name__}"
|
|
88
|
-
)
|
|
89
|
-
return raw.astype(rio.float32), raw, nodata
|
|
75
|
+
return BlockSource.from_dataset(other, band=1)
|
|
76
|
+
if isinstance(spec, (int, str)) and not isinstance(spec, bool):
|
|
77
|
+
return BlockSource.from_dataset(ds, band=resolve_band_index(ds, spec))
|
|
78
|
+
raise ValidationError(
|
|
79
|
+
"band must be an EEORasterDataset, a 1-based int band index, or a "
|
|
80
|
+
f"band name; got {type(spec).__name__}"
|
|
81
|
+
)
|
|
90
82
|
|
|
91
83
|
|
|
92
84
|
def _compute_index(ds, band_specs, formula, *, auto_align, method, name=None):
|
|
93
|
-
"""Resolve band specs, apply ``formula
|
|
85
|
+
"""Resolve band specs, apply ``formula`` block-wise, and package the result.
|
|
94
86
|
|
|
95
87
|
``band_specs`` is the ordered list of band specs; the first is the primary
|
|
96
|
-
band. ``formula`` maps the list of float32 band arrays to a 2D result
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
band. ``formula`` maps the list of float32 band arrays to a 2D result, and
|
|
89
|
+
is called once per window rather than once over the whole scene, so an
|
|
90
|
+
index never holds more than a block of each band in memory. The result is
|
|
91
|
+
masked per the nodata contract (contagious across every band) and returned
|
|
92
|
+
as a single-band float32 ``EEORasterDataset`` whose band carries ``name``
|
|
93
|
+
(unnamed when ``name`` is None).
|
|
100
94
|
"""
|
|
101
95
|
ds = ds.to_rasterio()
|
|
102
|
-
|
|
103
|
-
_resolve_band(ds, spec, auto_align=auto_align, method=method) for spec in band_specs
|
|
104
|
-
]
|
|
105
|
-
floats = [band for band, _raw, _nodata in resolved]
|
|
106
|
-
operands = [(raw, nodata) for _band, raw, nodata in resolved]
|
|
107
|
-
primary_nodata = resolved[0][2]
|
|
96
|
+
sources = [_resolve_band(ds, spec, auto_align=auto_align, method=method) for spec in band_specs]
|
|
108
97
|
|
|
109
|
-
|
|
98
|
+
def compute(*blocks):
|
|
99
|
+
# The formulas are written against float32 bands; the engine masks
|
|
100
|
+
# against these raw blocks, which keep their source dtype.
|
|
101
|
+
return formula([block.astype(rio.float32) for block in blocks])
|
|
110
102
|
|
|
111
|
-
|
|
112
|
-
result, operands, fractional=True, ds_nodata=primary_nodata
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
data = index[np.newaxis, ...] if index.ndim == 2 else index
|
|
116
|
-
meta = ds.get_metadata().copy()
|
|
117
|
-
meta.update(
|
|
118
|
-
driver="GTiff",
|
|
119
|
-
dtype="float32",
|
|
120
|
-
nodata=out_nodata,
|
|
121
|
-
height=data.shape[-2],
|
|
122
|
-
width=data.shape[-1],
|
|
123
|
-
count=data.shape[0],
|
|
124
|
-
)
|
|
125
|
-
memfile = rio.io.MemoryFile()
|
|
126
|
-
out_ds = memfile.open(**meta)
|
|
127
|
-
out_ds.write(data)
|
|
128
|
-
result = EEORasterDataset.from_rasterio(out_ds)
|
|
103
|
+
result = apply_blockwise(ds, compute, sources=sources, fractional=True)
|
|
129
104
|
if name is not None:
|
|
130
105
|
result.set_band_name(1, name)
|
|
131
106
|
return result
|
|
@@ -189,7 +164,6 @@ def normalized_difference(
|
|
|
189
164
|
|
|
190
165
|
Notes
|
|
191
166
|
-----
|
|
192
|
-
Reads both rasters fully into memory rather than streaming block-wise.
|
|
193
167
|
Nodata pixels are masked before the ratio; separately, a zero denominator
|
|
194
168
|
(``ds + other == 0``) is guarded by setting those pixels to 0.
|
|
195
169
|
|
|
@@ -205,38 +179,19 @@ def normalized_difference(
|
|
|
205
179
|
else:
|
|
206
180
|
raise AlignmentError(_ALIGN_MISMATCH.format(other=other.get_shape(), ds=ds.get_shape()))
|
|
207
181
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
182
|
+
def difference(a_block, b_block):
|
|
183
|
+
# Masking is the engine's job and happens after this returns, so a
|
|
184
|
+
# masked pixel is NaN regardless of the ratio computed for it here.
|
|
185
|
+
a = a_block.astype(rio.float32)
|
|
186
|
+
b = b_block.astype(rio.float32)
|
|
187
|
+
return _safe_ratio(a - b, a + b)
|
|
214
188
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
nd,
|
|
220
|
-
[(a_raw, ds_nodata), (b_raw, other_nodata)],
|
|
189
|
+
result = apply_blockwise(
|
|
190
|
+
ds,
|
|
191
|
+
difference,
|
|
192
|
+
sources=[BlockSource.from_dataset(ds), BlockSource.from_dataset(other)],
|
|
221
193
|
fractional=True,
|
|
222
|
-
ds_nodata=ds_nodata,
|
|
223
194
|
)
|
|
224
|
-
|
|
225
|
-
meta = ds.get_metadata().copy()
|
|
226
|
-
meta.update(
|
|
227
|
-
driver="GTiff",
|
|
228
|
-
dtype="float32",
|
|
229
|
-
nodata=out_nodata,
|
|
230
|
-
height=nd.shape[-2],
|
|
231
|
-
width=nd.shape[-1],
|
|
232
|
-
count=nd.shape[0],
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
memfile = rio.io.MemoryFile()
|
|
236
|
-
out_ds = memfile.open(**meta)
|
|
237
|
-
out_ds.write(nd)
|
|
238
|
-
|
|
239
|
-
result = EEORasterDataset.from_rasterio(out_ds)
|
|
240
195
|
if name is not None:
|
|
241
196
|
if result.get_count() != 1:
|
|
242
197
|
raise ValidationError(
|
|
@@ -309,11 +264,6 @@ def ndvi(
|
|
|
309
264
|
If a band argument is not an ``EEORasterDataset``, an int index, or a
|
|
310
265
|
band name, or if a band name is unknown or matches more than one band.
|
|
311
266
|
|
|
312
|
-
Notes
|
|
313
|
-
-----
|
|
314
|
-
Reads the required bands fully into memory rather than streaming
|
|
315
|
-
block-wise.
|
|
316
|
-
|
|
317
267
|
Examples
|
|
318
268
|
--------
|
|
319
269
|
>>> ndvi = nir_ds.ndvi(red_ds) # separate band rasters
|
|
@@ -393,9 +343,7 @@ def ndwi(
|
|
|
393
343
|
|
|
394
344
|
Notes
|
|
395
345
|
-----
|
|
396
|
-
|
|
397
|
-
block-wise. This is McFeeters' water NDWI; the moisture variant is
|
|
398
|
-
:meth:`ndmi`.
|
|
346
|
+
This is McFeeters' water NDWI; the moisture variant is :meth:`ndmi`.
|
|
399
347
|
|
|
400
348
|
Examples
|
|
401
349
|
--------
|
|
@@ -475,8 +423,7 @@ def ndmi(
|
|
|
475
423
|
|
|
476
424
|
Notes
|
|
477
425
|
-----
|
|
478
|
-
|
|
479
|
-
block-wise. Sentinel-2's SWIR1 (B11) is 20 m; pass ``auto_align=True``
|
|
426
|
+
Sentinel-2's SWIR1 (B11) is 20 m; pass ``auto_align=True``
|
|
480
427
|
(the default) to resample it onto a 10 m NIR grid.
|
|
481
428
|
|
|
482
429
|
Examples
|
|
@@ -558,8 +505,7 @@ def ndbi(
|
|
|
558
505
|
|
|
559
506
|
Notes
|
|
560
507
|
-----
|
|
561
|
-
|
|
562
|
-
block-wise. Sentinel-2's SWIR1 (B11) is 20 m; pass ``auto_align=True``
|
|
508
|
+
Sentinel-2's SWIR1 (B11) is 20 m; pass ``auto_align=True``
|
|
563
509
|
(the default) to resample it onto a 10 m NIR grid.
|
|
564
510
|
|
|
565
511
|
Examples
|
|
@@ -648,11 +594,6 @@ def evi(
|
|
|
648
594
|
If a band argument is not an ``EEORasterDataset``, an int index, or a
|
|
649
595
|
band name, or if a band name is unknown or matches more than one band.
|
|
650
596
|
|
|
651
|
-
Notes
|
|
652
|
-
-----
|
|
653
|
-
Reads the required bands fully into memory rather than streaming
|
|
654
|
-
block-wise.
|
|
655
|
-
|
|
656
597
|
Examples
|
|
657
598
|
--------
|
|
658
599
|
>>> evi = nir_ds.evi(red_ds, blue_ds) # separate band rasters
|
|
@@ -740,11 +681,6 @@ def savi(
|
|
|
740
681
|
If a band argument is not an ``EEORasterDataset``, an int index, or a
|
|
741
682
|
band name, or if a band name is unknown or matches more than one band.
|
|
742
683
|
|
|
743
|
-
Notes
|
|
744
|
-
-----
|
|
745
|
-
Reads the required bands fully into memory rather than streaming
|
|
746
|
-
block-wise.
|
|
747
|
-
|
|
748
684
|
Examples
|
|
749
685
|
--------
|
|
750
686
|
>>> savi = nir_ds.savi(red_ds, soil_factor=0.5) # separate band rasters
|
|
@@ -1,14 +1,71 @@
|
|
|
1
|
-
"""Per-pixel statistics and coordinate sampling.
|
|
1
|
+
"""Per-pixel statistics and coordinate sampling.
|
|
2
|
+
|
|
3
|
+
Each of these reports a value *and* where it occurs, which is why they stream
|
|
4
|
+
rather than reduce: the value comes from a streaming reduction over the band
|
|
5
|
+
(:mod:`eeo.core.streaming`), and a second streaming pass finds the pixel that
|
|
6
|
+
carries it. Memory is bounded by the block in both, so a statistic can be taken
|
|
7
|
+
over a scene far larger than memory.
|
|
8
|
+
|
|
9
|
+
Ties are broken exactly as ``numpy.nanargmin`` breaks them — first in
|
|
10
|
+
row-major order — regardless of how the raster happens to be cut into blocks,
|
|
11
|
+
so a position never depends on the block shape.
|
|
12
|
+
"""
|
|
2
13
|
|
|
3
14
|
import numpy as np
|
|
15
|
+
from rasterio.windows import Window
|
|
4
16
|
|
|
5
|
-
from eeo.common import get_nodata,
|
|
17
|
+
from eeo.common import get_nodata, resolve_band_index
|
|
6
18
|
from eeo.core.core import EEORasterDataset
|
|
7
19
|
from eeo.core.decorators import eeo_raster_op
|
|
8
20
|
from eeo.core.exceptions import ValidationError
|
|
21
|
+
from eeo.core.streaming import stream_windows, valid_mean_std, valid_percentiles
|
|
9
22
|
|
|
10
23
|
Coordinate = tuple[float, float] | list[float]
|
|
11
24
|
|
|
25
|
+
_NO_VALID_PIXELS = "band {band} has no valid pixels, so {what} is undefined"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _locate(ds, band_idx, score, what):
|
|
29
|
+
"""Stream the band and return ``(value, row, col)`` minimising ``score``.
|
|
30
|
+
|
|
31
|
+
``score`` maps a block of pixels to a block of comparison values; the
|
|
32
|
+
smallest wins. Because a block is a contiguous rectangle, block-local
|
|
33
|
+
row-major order agrees with global row-major order inside it, so taking
|
|
34
|
+
the first minimum per block and then breaking ties on ``(row, col)``
|
|
35
|
+
reproduces ``numpy.nanargmin`` over the whole band for any block shape.
|
|
36
|
+
|
|
37
|
+
Returns the *pixel's* value, not its score, so a caller searching for the
|
|
38
|
+
pixel nearest a target gets the measurement rather than the distance.
|
|
39
|
+
"""
|
|
40
|
+
band = resolve_band_index(ds, band_idx)
|
|
41
|
+
best = None
|
|
42
|
+
for window, block, valid in stream_windows(ds, band):
|
|
43
|
+
if not valid.any():
|
|
44
|
+
continue
|
|
45
|
+
# Score in float64, never in the block's own dtype: negating a uint8
|
|
46
|
+
# or uint16 block to turn a maximum into a minimum wraps instead of
|
|
47
|
+
# changing sign, which silently returns the *smallest* pixel whenever
|
|
48
|
+
# the band contains a zero.
|
|
49
|
+
scored = np.where(valid, score(block.astype(np.float64)), np.inf)
|
|
50
|
+
flat = int(np.argmin(scored))
|
|
51
|
+
local_row, local_col = np.unravel_index(flat, scored.shape)
|
|
52
|
+
row = int(window.row_off) + int(local_row)
|
|
53
|
+
col = int(window.col_off) + int(local_col)
|
|
54
|
+
candidate = (float(scored[local_row, local_col]), row, col)
|
|
55
|
+
if best is None or candidate < best[:3]:
|
|
56
|
+
best = (*candidate, block[local_row, local_col])
|
|
57
|
+
if best is None:
|
|
58
|
+
raise ValidationError(_NO_VALID_PIXELS.format(band=band_idx, what=what))
|
|
59
|
+
_score, row, col, value = best
|
|
60
|
+
return value, row, col
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _position(ds, row, col, as_pixel_coordinate):
|
|
64
|
+
"""Render a pixel location as ``(row, col)`` or as world coordinates."""
|
|
65
|
+
if as_pixel_coordinate:
|
|
66
|
+
return (row, col)
|
|
67
|
+
return ds.get_transform() * (col, row)
|
|
68
|
+
|
|
12
69
|
|
|
13
70
|
@eeo_raster_op
|
|
14
71
|
def extract_value_at_coordinate(
|
|
@@ -45,8 +102,9 @@ def extract_value_at_coordinate(
|
|
|
45
102
|
|
|
46
103
|
Notes
|
|
47
104
|
-----
|
|
48
|
-
Reads
|
|
49
|
-
|
|
105
|
+
Reads a single pixel, not the band: sampling one location never costs the
|
|
106
|
+
scene. Coordinates are ``(x, y)`` in CRS units, distinct from the
|
|
107
|
+
``(row, col)`` pixel indexing used elsewhere.
|
|
50
108
|
|
|
51
109
|
Examples
|
|
52
110
|
--------
|
|
@@ -67,7 +125,10 @@ def extract_value_at_coordinate(
|
|
|
67
125
|
row, col = backend.index(x, y)
|
|
68
126
|
row, col = int(row), int(col)
|
|
69
127
|
|
|
70
|
-
|
|
128
|
+
# A 1x1 window, so sampling a point in a 10980x10980 scene reads one
|
|
129
|
+
# pixel rather than the 241 MB band around it.
|
|
130
|
+
window = Window(col, row, 1, 1)
|
|
131
|
+
value = ds.read(resolve_band_index(ds, band_idx), window=window)[0, 0]
|
|
71
132
|
|
|
72
133
|
# Report nodata as NaN rather than the raw pixel value, so a fill value that
|
|
73
134
|
# sits near real measurements is never mistaken for one.
|
|
@@ -118,26 +179,22 @@ def get_maximum_pixel(
|
|
|
118
179
|
|
|
119
180
|
Notes
|
|
120
181
|
-----
|
|
121
|
-
|
|
182
|
+
Streams the band block by block, so memory is bounded by the block rather
|
|
183
|
+
than the band. Nodata pixels are excluded.
|
|
122
184
|
|
|
123
185
|
Examples
|
|
124
186
|
--------
|
|
125
187
|
>>> peak = ds.get_maximum_pixel()
|
|
126
188
|
>>> peak["value"], peak["position"]
|
|
127
189
|
"""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
#
|
|
131
|
-
value =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
else:
|
|
137
|
-
transform = ds.get_transform()
|
|
138
|
-
position = transform * (col, row)
|
|
139
|
-
|
|
140
|
-
return {"value": value, "position": position}
|
|
190
|
+
ds = ds.to_rasterio()
|
|
191
|
+
# Negating turns the search for a maximum into the same minimisation the
|
|
192
|
+
# other three do, tie-breaking included.
|
|
193
|
+
value, row, col = _locate(ds, band_idx, lambda block: -block, "a maximum")
|
|
194
|
+
return {
|
|
195
|
+
"value": float(value),
|
|
196
|
+
"position": _position(ds, row, col, return_position_as_pixel_coordinate),
|
|
197
|
+
}
|
|
141
198
|
|
|
142
199
|
|
|
143
200
|
@eeo_raster_op
|
|
@@ -178,26 +235,21 @@ def get_minimum_pixel(
|
|
|
178
235
|
|
|
179
236
|
Notes
|
|
180
237
|
-----
|
|
181
|
-
|
|
238
|
+
Streams the band block by block in two passes — one to measure, one to
|
|
239
|
+
locate — so memory is bounded by the block rather than the band. Nodata
|
|
240
|
+
pixels are excluded from both.
|
|
182
241
|
|
|
183
242
|
Examples
|
|
184
243
|
--------
|
|
185
244
|
>>> low = ds.get_minimum_pixel()
|
|
186
245
|
>>> low["value"], low["position"]
|
|
187
246
|
"""
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if return_position_as_pixel_coordinate:
|
|
195
|
-
position = (row, col)
|
|
196
|
-
else:
|
|
197
|
-
transform = ds.get_transform()
|
|
198
|
-
position = transform * (col, row)
|
|
199
|
-
|
|
200
|
-
return {"value": value, "position": position}
|
|
247
|
+
ds = ds.to_rasterio()
|
|
248
|
+
value, row, col = _locate(ds, band_idx, lambda block: block, "a minimum")
|
|
249
|
+
return {
|
|
250
|
+
"value": float(value),
|
|
251
|
+
"position": _position(ds, row, col, return_position_as_pixel_coordinate),
|
|
252
|
+
}
|
|
201
253
|
|
|
202
254
|
|
|
203
255
|
@eeo_raster_op
|
|
@@ -239,27 +291,22 @@ def get_mean_pixel(
|
|
|
239
291
|
|
|
240
292
|
Notes
|
|
241
293
|
-----
|
|
242
|
-
|
|
294
|
+
Streams the band block by block in two passes — one to measure, one to
|
|
295
|
+
locate — so memory is bounded by the block rather than the band. Nodata
|
|
296
|
+
pixels are excluded from both.
|
|
243
297
|
|
|
244
298
|
Examples
|
|
245
299
|
--------
|
|
246
300
|
>>> centre = ds.get_mean_pixel()
|
|
247
301
|
>>> centre["value"], centre["position"]
|
|
248
302
|
"""
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if return_position_as_pixel_coordinate:
|
|
257
|
-
position = (row, col)
|
|
258
|
-
else:
|
|
259
|
-
transform = ds.get_transform()
|
|
260
|
-
position = transform * (col, row)
|
|
261
|
-
|
|
262
|
-
return {"value": mean_value, "position": position}
|
|
303
|
+
ds = ds.to_rasterio()
|
|
304
|
+
mean_value, _std = valid_mean_std(ds, resolve_band_index(ds, band_idx))
|
|
305
|
+
_value, row, col = _locate(ds, band_idx, lambda block: np.abs(block - mean_value), "a mean")
|
|
306
|
+
return {
|
|
307
|
+
"value": mean_value,
|
|
308
|
+
"position": _position(ds, row, col, return_position_as_pixel_coordinate),
|
|
309
|
+
}
|
|
263
310
|
|
|
264
311
|
|
|
265
312
|
@eeo_raster_op
|
|
@@ -304,24 +351,21 @@ def get_percentile_pixel(
|
|
|
304
351
|
|
|
305
352
|
Notes
|
|
306
353
|
-----
|
|
307
|
-
|
|
354
|
+
Streams the band block by block in two passes — one to measure, one to
|
|
355
|
+
locate — so memory is bounded by the block rather than the band. Nodata
|
|
356
|
+
pixels are excluded from both.
|
|
308
357
|
|
|
309
358
|
Examples
|
|
310
359
|
--------
|
|
311
360
|
>>> p95 = ds.get_percentile_pixel(95)
|
|
312
361
|
>>> p95["value"], p95["position"]
|
|
313
362
|
"""
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
else:
|
|
324
|
-
transform = ds.get_transform()
|
|
325
|
-
position = transform * (col, row)
|
|
326
|
-
|
|
327
|
-
return {"value": perc_value, "position": position}
|
|
363
|
+
ds = ds.to_rasterio()
|
|
364
|
+
(perc_value,) = valid_percentiles(ds, (percentile,), resolve_band_index(ds, band_idx))
|
|
365
|
+
_value, row, col = _locate(
|
|
366
|
+
ds, band_idx, lambda block: np.abs(block - perc_value), "a percentile"
|
|
367
|
+
)
|
|
368
|
+
return {
|
|
369
|
+
"value": perc_value,
|
|
370
|
+
"position": _position(ds, row, col, return_position_as_pixel_coordinate),
|
|
371
|
+
}
|
|
@@ -19,12 +19,11 @@ def is_rasterio_backed(ds: EEORasterDataset) -> bool:
|
|
|
19
19
|
"""Return True if ``ds`` is backed by the rasterio adapter.
|
|
20
20
|
|
|
21
21
|
Detection is based on the adapter type, not the class of the backend
|
|
22
|
-
object. A rasterio-backed dataset's ``backend``
|
|
23
|
-
``rasterio.io.DatasetReader``
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
the writer case and wrongly rejects genuinely rasterio-backed datasets.
|
|
22
|
+
object. A rasterio-backed dataset's ``backend`` is usually a
|
|
23
|
+
``rasterio.io.DatasetReader`` — from a file, or an in-memory result, which
|
|
24
|
+
the library reopens read-only — but a ``rasterio.io.DatasetWriter`` wrapped
|
|
25
|
+
with ``EEORasterDataset.from_rasterio`` is equally valid. Checking
|
|
26
|
+
``isinstance(backend, DatasetReader)`` would wrongly reject that case.
|
|
28
27
|
|
|
29
28
|
Parameters
|
|
30
29
|
----------
|
|
@@ -164,7 +163,7 @@ def _declared_nodata_mask(array, nodata):
|
|
|
164
163
|
return array == nodata
|
|
165
164
|
|
|
166
165
|
|
|
167
|
-
def
|
|
166
|
+
def resolve_output_dtype(result, *, fractional: bool):
|
|
168
167
|
"""Return the output dtype for a pixel-wise result per the dtype policy.
|
|
169
168
|
|
|
170
169
|
Fractional-result ops are always float32. Exact arithmetic keeps the
|
|
@@ -178,63 +177,85 @@ def _output_dtype(result, *, fractional: bool):
|
|
|
178
177
|
return dtype
|
|
179
178
|
|
|
180
179
|
|
|
181
|
-
def
|
|
182
|
-
"""
|
|
180
|
+
def _combined_nodata_mask(operands):
|
|
181
|
+
"""Boolean mask of pixels that are nodata in any operand, or None if none are.
|
|
182
|
+
|
|
183
|
+
Nodata is contagious: the masks of every operand that declares one are
|
|
184
|
+
OR-ed together. Returns None when no operand declares a nodata value,
|
|
185
|
+
which is the same condition as the contract producing no output nodata.
|
|
186
|
+
"""
|
|
187
|
+
combined = None
|
|
188
|
+
for array, nodata in operands:
|
|
189
|
+
mask = _declared_nodata_mask(array, nodata)
|
|
190
|
+
if mask is None:
|
|
191
|
+
continue
|
|
192
|
+
combined = mask if combined is None else (combined | mask)
|
|
193
|
+
return combined
|
|
183
194
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
195
|
+
|
|
196
|
+
def resolve_output_nodata(operand_nodatas, *, out_dtype, ds_nodata):
|
|
197
|
+
"""Return the nodata value a pixel-wise result should record, or None.
|
|
198
|
+
|
|
199
|
+
Decided from declared nodata values and the output dtype alone — no pixel
|
|
200
|
+
data — so a block-wise operation can fix one nodata value for the whole
|
|
201
|
+
output before reading the first block.
|
|
187
202
|
|
|
188
203
|
Parameters
|
|
189
204
|
----------
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
operands : list of tuple
|
|
195
|
-
``(array, nodata)`` for each raster operand; scalar operands are
|
|
196
|
-
omitted since they carry no nodata.
|
|
197
|
-
fractional : bool
|
|
198
|
-
True for operations whose result is inherently fractional (float32
|
|
199
|
-
output regardless of input dtype).
|
|
205
|
+
operand_nodatas : iterable
|
|
206
|
+
Declared nodata value (or None) of each raster operand.
|
|
207
|
+
out_dtype : numpy.dtype
|
|
208
|
+
Dtype the output will be written in.
|
|
200
209
|
ds_nodata : int, float, or None
|
|
201
210
|
The primary operand's declared nodata, used as the sentinel for
|
|
202
211
|
integer outputs.
|
|
203
212
|
|
|
204
213
|
Returns
|
|
205
214
|
-------
|
|
206
|
-
|
|
207
|
-
``(
|
|
208
|
-
|
|
209
|
-
floating outputs, the integer sentinel for integer outputs, or None
|
|
210
|
-
when no operand declares nodata).
|
|
215
|
+
float or int or None
|
|
216
|
+
``float('nan')`` for floating outputs, the integer sentinel for
|
|
217
|
+
integer outputs, or None when no operand declares nodata.
|
|
211
218
|
"""
|
|
212
|
-
|
|
213
|
-
|
|
219
|
+
declared = [nodata for nodata in operand_nodatas if nodata is not None]
|
|
220
|
+
if not declared:
|
|
221
|
+
# No operand declared nodata: every pixel is valid, nothing to record.
|
|
222
|
+
return None
|
|
223
|
+
if np.issubdtype(out_dtype, np.floating):
|
|
224
|
+
return float("nan")
|
|
225
|
+
sentinel = ds_nodata if ds_nodata is not None else declared[0]
|
|
226
|
+
return np.array(sentinel, dtype=out_dtype).item()
|
|
214
227
|
|
|
215
|
-
combined = None
|
|
216
|
-
declared = []
|
|
217
|
-
for array, nodata in operands:
|
|
218
|
-
if nodata is not None:
|
|
219
|
-
declared.append(nodata)
|
|
220
|
-
mask = _declared_nodata_mask(array, nodata)
|
|
221
|
-
if mask is None:
|
|
222
|
-
continue
|
|
223
|
-
combined = mask if combined is None else (combined | mask)
|
|
224
228
|
|
|
225
|
-
|
|
229
|
+
def apply_nodata_mask(result, operands, *, out_dtype, out_nodata):
|
|
230
|
+
"""Cast a pixel-wise result to ``out_dtype`` and mark its nodata pixels.
|
|
231
|
+
|
|
232
|
+
The dtype and nodata value are supplied rather than derived, so every
|
|
233
|
+
block of a block-wise run lands in the same dtype and uses the same
|
|
234
|
+
marker even when a block happens to contain no nodata pixels at all.
|
|
226
235
|
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
result : array-like
|
|
239
|
+
Values computed over every pixel of the block.
|
|
240
|
+
operands : list of tuple
|
|
241
|
+
``(array, nodata)`` for each raster operand, over the same pixels as
|
|
242
|
+
``result``; scalar operands are omitted since they carry no nodata.
|
|
243
|
+
out_dtype : numpy.dtype
|
|
244
|
+
Dtype to cast the result to.
|
|
245
|
+
out_nodata : float, int, or None
|
|
246
|
+
Marker written into nodata pixels, from :func:`resolve_output_nodata`.
|
|
247
|
+
None means no operand declares nodata and nothing is masked.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
array-like
|
|
252
|
+
The masked result in ``out_dtype``.
|
|
253
|
+
"""
|
|
254
|
+
result = result.astype(out_dtype)
|
|
255
|
+
if out_nodata is None:
|
|
256
|
+
return result
|
|
257
|
+
combined = _combined_nodata_mask(operands)
|
|
227
258
|
if combined is None:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if is_float_out:
|
|
232
|
-
marker = np.array(np.nan, dtype=out_dtype)
|
|
233
|
-
out_nodata: float = float("nan")
|
|
234
|
-
else:
|
|
235
|
-
sentinel = ds_nodata if ds_nodata is not None else declared[0]
|
|
236
|
-
marker = np.array(sentinel, dtype=out_dtype)
|
|
237
|
-
out_nodata = marker.item()
|
|
238
|
-
|
|
239
|
-
final = np.where(combined, marker, result).astype(out_dtype)
|
|
240
|
-
return final, out_nodata
|
|
259
|
+
return result
|
|
260
|
+
marker = np.array(out_nodata, dtype=out_dtype)
|
|
261
|
+
return np.where(combined, marker, result).astype(out_dtype)
|