mapchete-eo 2025.7.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mapchete_eo/__init__.py +1 -0
- mapchete_eo/archives/__init__.py +0 -0
- mapchete_eo/archives/base.py +65 -0
- mapchete_eo/array/__init__.py +0 -0
- mapchete_eo/array/buffer.py +16 -0
- mapchete_eo/array/color.py +29 -0
- mapchete_eo/array/convert.py +157 -0
- mapchete_eo/base.py +528 -0
- mapchete_eo/blacklist.txt +175 -0
- mapchete_eo/cli/__init__.py +30 -0
- mapchete_eo/cli/bounds.py +22 -0
- mapchete_eo/cli/options_arguments.py +243 -0
- mapchete_eo/cli/s2_brdf.py +77 -0
- mapchete_eo/cli/s2_cat_results.py +146 -0
- mapchete_eo/cli/s2_find_broken_products.py +93 -0
- mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
- mapchete_eo/cli/s2_mask.py +71 -0
- mapchete_eo/cli/s2_mgrs.py +45 -0
- mapchete_eo/cli/s2_rgb.py +114 -0
- mapchete_eo/cli/s2_verify.py +129 -0
- mapchete_eo/cli/static_catalog.py +123 -0
- mapchete_eo/eostac.py +30 -0
- mapchete_eo/exceptions.py +87 -0
- mapchete_eo/geometry.py +271 -0
- mapchete_eo/image_operations/__init__.py +12 -0
- mapchete_eo/image_operations/color_correction.py +136 -0
- mapchete_eo/image_operations/compositing.py +247 -0
- mapchete_eo/image_operations/dtype_scale.py +43 -0
- mapchete_eo/image_operations/fillnodata.py +130 -0
- mapchete_eo/image_operations/filters.py +319 -0
- mapchete_eo/image_operations/linear_normalization.py +81 -0
- mapchete_eo/image_operations/sigmoidal.py +114 -0
- mapchete_eo/io/__init__.py +37 -0
- mapchete_eo/io/assets.py +492 -0
- mapchete_eo/io/items.py +147 -0
- mapchete_eo/io/levelled_cubes.py +228 -0
- mapchete_eo/io/path.py +144 -0
- mapchete_eo/io/products.py +413 -0
- mapchete_eo/io/profiles.py +45 -0
- mapchete_eo/known_catalogs.py +42 -0
- mapchete_eo/platforms/sentinel2/__init__.py +17 -0
- mapchete_eo/platforms/sentinel2/archives.py +190 -0
- mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
- mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
- mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
- mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
- mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
- mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
- mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
- mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
- mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
- mapchete_eo/platforms/sentinel2/config.py +181 -0
- mapchete_eo/platforms/sentinel2/driver.py +78 -0
- mapchete_eo/platforms/sentinel2/masks.py +325 -0
- mapchete_eo/platforms/sentinel2/metadata_parser.py +734 -0
- mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +29 -0
- mapchete_eo/platforms/sentinel2/path_mappers/base.py +56 -0
- mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +34 -0
- mapchete_eo/platforms/sentinel2/path_mappers/metadata_xml.py +135 -0
- mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +105 -0
- mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +26 -0
- mapchete_eo/platforms/sentinel2/processing_baseline.py +160 -0
- mapchete_eo/platforms/sentinel2/product.py +669 -0
- mapchete_eo/platforms/sentinel2/types.py +109 -0
- mapchete_eo/processes/__init__.py +0 -0
- mapchete_eo/processes/config.py +51 -0
- mapchete_eo/processes/dtype_scale.py +112 -0
- mapchete_eo/processes/eo_to_xarray.py +19 -0
- mapchete_eo/processes/merge_rasters.py +235 -0
- mapchete_eo/product.py +278 -0
- mapchete_eo/protocols.py +56 -0
- mapchete_eo/search/__init__.py +14 -0
- mapchete_eo/search/base.py +222 -0
- mapchete_eo/search/config.py +42 -0
- mapchete_eo/search/s2_mgrs.py +314 -0
- mapchete_eo/search/stac_search.py +251 -0
- mapchete_eo/search/stac_static.py +236 -0
- mapchete_eo/search/utm_search.py +251 -0
- mapchete_eo/settings.py +24 -0
- mapchete_eo/sort.py +48 -0
- mapchete_eo/time.py +53 -0
- mapchete_eo/types.py +73 -0
- mapchete_eo-2025.7.0.dist-info/METADATA +38 -0
- mapchete_eo-2025.7.0.dist-info/RECORD +87 -0
- mapchete_eo-2025.7.0.dist-info/WHEEL +5 -0
- mapchete_eo-2025.7.0.dist-info/entry_points.txt +11 -0
- mapchete_eo-2025.7.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import numpy.ma as ma
|
|
8
|
+
import pystac
|
|
9
|
+
from mapchete.io.raster import ReferencedRaster, read_raster_window, resample_from_array
|
|
10
|
+
from mapchete.geometry import reproject_geometry
|
|
11
|
+
from mapchete.path import MPath
|
|
12
|
+
from mapchete.protocols import GridProtocol
|
|
13
|
+
from mapchete.types import Bounds, Grid, NodataVals
|
|
14
|
+
from rasterio.enums import Resampling
|
|
15
|
+
from rasterio.features import rasterize
|
|
16
|
+
from shapely.geometry import shape
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from mapchete_eo.array.buffer import buffer_array
|
|
20
|
+
from mapchete_eo.platforms.sentinel2.brdf.config import BRDFModels
|
|
21
|
+
from mapchete_eo.platforms.sentinel2.brdf.correction import apply_correction
|
|
22
|
+
from mapchete_eo.exceptions import (
|
|
23
|
+
AllMasked,
|
|
24
|
+
AssetError,
|
|
25
|
+
BRDFError,
|
|
26
|
+
CorruptedProduct,
|
|
27
|
+
EmptyFootprintException,
|
|
28
|
+
EmptyProductException,
|
|
29
|
+
)
|
|
30
|
+
from mapchete_eo.geometry import buffer_antimeridian_safe
|
|
31
|
+
from mapchete_eo.io.assets import get_assets, read_mask_as_raster
|
|
32
|
+
from mapchete_eo.io.path import asset_mpath, get_product_cache_path
|
|
33
|
+
from mapchete_eo.io.profiles import COGDeflateProfile
|
|
34
|
+
from mapchete_eo.platforms.sentinel2.brdf import correction_values
|
|
35
|
+
from mapchete_eo.platforms.sentinel2.bandpass_adjustment import (
|
|
36
|
+
apply_bandpass_adjustment,
|
|
37
|
+
)
|
|
38
|
+
from mapchete_eo.platforms.sentinel2.config import (
|
|
39
|
+
BRDFConfig,
|
|
40
|
+
BRDFModelConfig,
|
|
41
|
+
CacheConfig,
|
|
42
|
+
MaskConfig,
|
|
43
|
+
)
|
|
44
|
+
from mapchete_eo.platforms.sentinel2.metadata_parser import S2Metadata
|
|
45
|
+
from mapchete_eo.platforms.sentinel2.types import (
|
|
46
|
+
CloudType,
|
|
47
|
+
L2ABand,
|
|
48
|
+
ProductQIMaskResolution,
|
|
49
|
+
Resolution,
|
|
50
|
+
)
|
|
51
|
+
from mapchete_eo.product import EOProduct, add_to_blacklist
|
|
52
|
+
from mapchete_eo.protocols import EOProductProtocol
|
|
53
|
+
from mapchete_eo.settings import mapchete_eo_settings
|
|
54
|
+
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Cache:
|
|
59
|
+
item: pystac.Item
|
|
60
|
+
config: CacheConfig
|
|
61
|
+
path: MPath
|
|
62
|
+
|
|
63
|
+
def __init__(self, item: pystac.Item, config: CacheConfig):
|
|
64
|
+
self.item = item
|
|
65
|
+
self.config = config
|
|
66
|
+
# TODO: maybe move this function here
|
|
67
|
+
self.path = get_product_cache_path(
|
|
68
|
+
self.item,
|
|
69
|
+
MPath.from_inp(self.config.path),
|
|
70
|
+
self.config.product_path_generation_method,
|
|
71
|
+
)
|
|
72
|
+
self.path.makedirs()
|
|
73
|
+
self._brdf_grid_cache: dict = dict()
|
|
74
|
+
if self.config.brdf:
|
|
75
|
+
self._brdf_bands = [
|
|
76
|
+
asset_name_to_l2a_band(self.item, band)
|
|
77
|
+
for band in self.config.brdf.bands
|
|
78
|
+
]
|
|
79
|
+
else:
|
|
80
|
+
self._brdf_bands = []
|
|
81
|
+
try:
|
|
82
|
+
self._existing_files = self.path.ls()
|
|
83
|
+
except FileNotFoundError:
|
|
84
|
+
self._existing_files = None
|
|
85
|
+
|
|
86
|
+
def __repr__(self):
|
|
87
|
+
return f"<Cache: product={self.item.id}, path={self.path}>"
|
|
88
|
+
|
|
89
|
+
def cache_assets(self):
|
|
90
|
+
# cache assets
|
|
91
|
+
if self.config.assets:
|
|
92
|
+
# TODO determine already existing assets
|
|
93
|
+
self.item = get_assets(
|
|
94
|
+
self.item,
|
|
95
|
+
self.config.assets,
|
|
96
|
+
self.path,
|
|
97
|
+
resolution=self.config.assets_resolution.value,
|
|
98
|
+
ignore_if_exists=True,
|
|
99
|
+
item_href_in_dst_dir=False,
|
|
100
|
+
)
|
|
101
|
+
return self.item
|
|
102
|
+
|
|
103
|
+
def cache_brdf_grids(self, metadata: S2Metadata):
|
|
104
|
+
if self.config.brdf:
|
|
105
|
+
resolution = self.config.brdf.resolution
|
|
106
|
+
model = self.config.brdf.model
|
|
107
|
+
|
|
108
|
+
logger.debug(
|
|
109
|
+
f"prepare BRDF model '{model}' for product bands {self._brdf_bands} in {resolution} resolution"
|
|
110
|
+
)
|
|
111
|
+
for band in self._brdf_bands:
|
|
112
|
+
out_path = self.path / f"brdf_{model}_{band.name}_{resolution}.tif"
|
|
113
|
+
# TODO: do check with _existing_files again to reduce S3 requests
|
|
114
|
+
if not out_path.exists():
|
|
115
|
+
try:
|
|
116
|
+
grid = correction_values(
|
|
117
|
+
metadata,
|
|
118
|
+
band,
|
|
119
|
+
model=model,
|
|
120
|
+
resolution=resolution,
|
|
121
|
+
per_detector=self.config.brdf.per_detector_correction,
|
|
122
|
+
)
|
|
123
|
+
except BRDFError as exc:
|
|
124
|
+
error_msg = (
|
|
125
|
+
f"product {self.item.get_self_href()} is corrupted: {exc}"
|
|
126
|
+
)
|
|
127
|
+
logger.error(error_msg)
|
|
128
|
+
add_to_blacklist(self.item.get_self_href())
|
|
129
|
+
raise CorruptedProduct(error_msg)
|
|
130
|
+
|
|
131
|
+
logger.debug("cache BRDF correction grid to %s", out_path)
|
|
132
|
+
grid.to_file(out_path, **COGDeflateProfile(grid.meta))
|
|
133
|
+
self._brdf_grid_cache[band] = out_path
|
|
134
|
+
|
|
135
|
+
def get_brdf_grid(self, band: L2ABand):
|
|
136
|
+
try:
|
|
137
|
+
return self._brdf_grid_cache[band]
|
|
138
|
+
except KeyError:
|
|
139
|
+
if band in self._brdf_bands:
|
|
140
|
+
raise KeyError(f"BRDF grid for band {band} not yet cached")
|
|
141
|
+
else:
|
|
142
|
+
raise KeyError(f"BRDF grid for band {band} not configured")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class S2Product(EOProduct, EOProductProtocol):
|
|
146
|
+
item_dict: dict
|
|
147
|
+
cache: Optional[Cache] = None
|
|
148
|
+
_scl_cache: Dict[GridProtocol, np.ndarray]
|
|
149
|
+
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
item: pystac.Item,
|
|
153
|
+
metadata: Optional[S2Metadata] = None,
|
|
154
|
+
cache_config: Optional[CacheConfig] = None,
|
|
155
|
+
):
|
|
156
|
+
self.item_dict = item.to_dict()
|
|
157
|
+
self.id = item.id
|
|
158
|
+
|
|
159
|
+
self._metadata = metadata
|
|
160
|
+
self._scl_cache = dict()
|
|
161
|
+
self.cache = Cache(item, cache_config) if cache_config else None
|
|
162
|
+
|
|
163
|
+
self.__geo_interface__ = item.geometry
|
|
164
|
+
self.bounds = Bounds.from_inp(shape(self))
|
|
165
|
+
self.crs = mapchete_eo_settings.default_catalog_crs
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def from_stac_item(
|
|
169
|
+
self,
|
|
170
|
+
item: pystac.Item,
|
|
171
|
+
cache_config: Optional[CacheConfig] = None,
|
|
172
|
+
cache_all: bool = False,
|
|
173
|
+
**kwargs,
|
|
174
|
+
) -> S2Product:
|
|
175
|
+
s2product = S2Product(item, cache_config=cache_config)
|
|
176
|
+
|
|
177
|
+
if cache_all:
|
|
178
|
+
# cache assets if configured
|
|
179
|
+
s2product.cache_assets()
|
|
180
|
+
|
|
181
|
+
# cache BRDF grids if configured
|
|
182
|
+
s2product.cache_brdf_grids()
|
|
183
|
+
|
|
184
|
+
return s2product
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def metadata(self) -> S2Metadata:
|
|
188
|
+
if not self._metadata:
|
|
189
|
+
self._metadata = S2Metadata.from_stac_item(
|
|
190
|
+
pystac.Item.from_dict(self.item_dict)
|
|
191
|
+
)
|
|
192
|
+
return self._metadata
|
|
193
|
+
|
|
194
|
+
def __repr__(self):
|
|
195
|
+
return f"<S2Product product_id={self.id}>"
|
|
196
|
+
|
|
197
|
+
def clear_cached_data(self):
|
|
198
|
+
logger.debug("clear S2Product caches")
|
|
199
|
+
if self._metadata is not None:
|
|
200
|
+
self._metadata.clear_cached_data()
|
|
201
|
+
self._metadata = None
|
|
202
|
+
self._scl_cache = dict()
|
|
203
|
+
|
|
204
|
+
def read_np_array(
|
|
205
|
+
self,
|
|
206
|
+
assets: Optional[List[str]] = None,
|
|
207
|
+
eo_bands: Optional[List[str]] = None,
|
|
208
|
+
grid: Union[GridProtocol, Resolution] = Resolution["10m"],
|
|
209
|
+
resampling: Resampling = Resampling.nearest,
|
|
210
|
+
nodatavals: NodataVals = None,
|
|
211
|
+
raise_empty: bool = True,
|
|
212
|
+
apply_offset: bool = True,
|
|
213
|
+
apply_scale: bool = False,
|
|
214
|
+
apply_sentinel2_bandpass_adjustment: bool = False,
|
|
215
|
+
mask_config: MaskConfig = MaskConfig(),
|
|
216
|
+
brdf_config: Optional[BRDFConfig] = None,
|
|
217
|
+
fill_value: int = 0,
|
|
218
|
+
target_mask: Optional[np.ndarray] = None,
|
|
219
|
+
**kwargs,
|
|
220
|
+
) -> ma.MaskedArray:
|
|
221
|
+
assets = assets or []
|
|
222
|
+
eo_bands = eo_bands or []
|
|
223
|
+
apply_offset = apply_offset and not self.metadata.boa_offset_applied
|
|
224
|
+
if eo_bands:
|
|
225
|
+
count = len(eo_bands)
|
|
226
|
+
raise NotImplementedError("please use asset names for now")
|
|
227
|
+
else:
|
|
228
|
+
count = len(assets)
|
|
229
|
+
if isinstance(grid, Resolution):
|
|
230
|
+
grid = self.metadata.grid(grid)
|
|
231
|
+
mask = self.get_mask(grid, mask_config, target_mask=target_mask).data
|
|
232
|
+
if nodatavals is None:
|
|
233
|
+
nodatavals = fill_value
|
|
234
|
+
elif fill_value is None and nodatavals is not None:
|
|
235
|
+
fill_value = nodatavals
|
|
236
|
+
if mask.all():
|
|
237
|
+
if raise_empty:
|
|
238
|
+
raise EmptyProductException(
|
|
239
|
+
f"{self}: configured mask over {grid} covers everything"
|
|
240
|
+
)
|
|
241
|
+
else:
|
|
242
|
+
return self.empty_array(count, grid=grid, fill_value=fill_value)
|
|
243
|
+
|
|
244
|
+
arr = super().read_np_array(
|
|
245
|
+
assets=assets,
|
|
246
|
+
eo_bands=eo_bands,
|
|
247
|
+
grid=grid,
|
|
248
|
+
resampling=resampling,
|
|
249
|
+
raise_empty=False,
|
|
250
|
+
apply_offset=apply_offset,
|
|
251
|
+
apply_scale=apply_scale,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# bring mask to same shape as data array
|
|
255
|
+
expanded_mask = np.repeat(np.expand_dims(mask, axis=0), arr.shape[0], axis=0)
|
|
256
|
+
arr.set_fill_value(fill_value)
|
|
257
|
+
arr[expanded_mask] = fill_value
|
|
258
|
+
arr[expanded_mask] = ma.masked
|
|
259
|
+
|
|
260
|
+
if arr.mask.all():
|
|
261
|
+
if raise_empty:
|
|
262
|
+
raise EmptyProductException(
|
|
263
|
+
f"{self}: is empty over {grid} after reading bands and applying all masks"
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
return self.empty_array(count, grid=grid, fill_value=fill_value)
|
|
267
|
+
|
|
268
|
+
# apply Sentinel-2 bandpass adjustment
|
|
269
|
+
if apply_sentinel2_bandpass_adjustment:
|
|
270
|
+
arr = self._apply_sentinel2_bandpass_adjustment(
|
|
271
|
+
uncorrected=arr, assets=assets
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# apply BRDF config if required
|
|
275
|
+
if brdf_config:
|
|
276
|
+
arr = self._apply_brdf(
|
|
277
|
+
uncorrected=arr,
|
|
278
|
+
assets=assets,
|
|
279
|
+
brdf_config=brdf_config,
|
|
280
|
+
grid=grid,
|
|
281
|
+
resampling=resampling,
|
|
282
|
+
mask_config=mask_config,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
return ma.MaskedArray(arr, fill_value=fill_value)
|
|
286
|
+
|
|
287
|
+
def cache_assets(self) -> None:
|
|
288
|
+
if self.cache is not None:
|
|
289
|
+
self.cache.cache_assets()
|
|
290
|
+
|
|
291
|
+
def cache_brdf_grids(self) -> None:
|
|
292
|
+
if self.cache is not None:
|
|
293
|
+
self.cache.cache_brdf_grids(self.metadata)
|
|
294
|
+
|
|
295
|
+
def read_brdf_grid(
|
|
296
|
+
self,
|
|
297
|
+
band: L2ABand,
|
|
298
|
+
resampling: Resampling = Resampling.bilinear,
|
|
299
|
+
grid: Union[GridProtocol, Resolution] = Resolution["20m"],
|
|
300
|
+
brdf_config: BRDFModelConfig = BRDFConfig(),
|
|
301
|
+
) -> np.ndarray:
|
|
302
|
+
grid = (
|
|
303
|
+
self.metadata.grid(grid)
|
|
304
|
+
if isinstance(grid, Resolution)
|
|
305
|
+
else Grid.from_obj(grid)
|
|
306
|
+
)
|
|
307
|
+
try:
|
|
308
|
+
# read cached file if configured
|
|
309
|
+
if self.cache:
|
|
310
|
+
return read_raster_window(
|
|
311
|
+
self.cache.get_brdf_grid(band),
|
|
312
|
+
grid=grid,
|
|
313
|
+
resampling=resampling,
|
|
314
|
+
)
|
|
315
|
+
# calculate on the fly
|
|
316
|
+
return resample_from_array(
|
|
317
|
+
correction_values(
|
|
318
|
+
self.metadata,
|
|
319
|
+
band,
|
|
320
|
+
model=brdf_config.model,
|
|
321
|
+
resolution=brdf_config.resolution,
|
|
322
|
+
footprints_cached_read=brdf_config.footprints_cached_read,
|
|
323
|
+
per_detector=brdf_config.per_detector_correction,
|
|
324
|
+
),
|
|
325
|
+
out_grid=grid,
|
|
326
|
+
resampling=resampling,
|
|
327
|
+
keep_2d=True,
|
|
328
|
+
)
|
|
329
|
+
except (AssetError, BRDFError) as exc:
|
|
330
|
+
error_msg = f"product {self.item.get_self_href()} is corrupted: {exc}"
|
|
331
|
+
logger.error(error_msg)
|
|
332
|
+
add_to_blacklist(self.item.get_self_href())
|
|
333
|
+
raise CorruptedProduct(error_msg)
|
|
334
|
+
|
|
335
|
+
def read_l1c_cloud_mask(
|
|
336
|
+
self,
|
|
337
|
+
grid: Union[GridProtocol, Resolution] = Resolution["20m"],
|
|
338
|
+
cloud_type: CloudType = CloudType.all,
|
|
339
|
+
cached_read: bool = False,
|
|
340
|
+
) -> ReferencedRaster:
|
|
341
|
+
"""Return classification cloud mask."""
|
|
342
|
+
logger.debug("read classification cloud mask for %s", str(self))
|
|
343
|
+
return self.metadata.l1c_cloud_mask(
|
|
344
|
+
cloud_type, dst_grid=grid, cached_read=cached_read
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def read_snow_ice_mask(
|
|
348
|
+
self,
|
|
349
|
+
grid: Union[GridProtocol, Resolution] = Resolution["20m"],
|
|
350
|
+
cached_read: bool = False,
|
|
351
|
+
) -> ReferencedRaster:
|
|
352
|
+
"""Return classification snow and ice mask."""
|
|
353
|
+
logger.debug("read classification snow and ice mask for %s", str(self))
|
|
354
|
+
return self.metadata.snow_ice_mask(dst_grid=grid, cached_read=cached_read)
|
|
355
|
+
|
|
356
|
+
def read_cloud_probability(
|
|
357
|
+
self,
|
|
358
|
+
grid: Union[GridProtocol, Resolution] = Resolution["20m"],
|
|
359
|
+
resampling: Resampling = Resampling.bilinear,
|
|
360
|
+
from_resolution: ProductQIMaskResolution = ProductQIMaskResolution["20m"],
|
|
361
|
+
cached_read: bool = False,
|
|
362
|
+
) -> ReferencedRaster:
|
|
363
|
+
"""Return cloud probability mask."""
|
|
364
|
+
logger.debug("read cloud probability mask for %s", str(self))
|
|
365
|
+
return self.metadata.cloud_probability(
|
|
366
|
+
dst_grid=grid,
|
|
367
|
+
resampling=resampling,
|
|
368
|
+
from_resolution=from_resolution,
|
|
369
|
+
cached_read=cached_read,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def read_snow_probability(
|
|
373
|
+
self,
|
|
374
|
+
grid: Union[GridProtocol, Resolution] = Resolution["20m"],
|
|
375
|
+
resampling: Resampling = Resampling.bilinear,
|
|
376
|
+
from_resolution: ProductQIMaskResolution = ProductQIMaskResolution["20m"],
|
|
377
|
+
cached_read: bool = False,
|
|
378
|
+
) -> ReferencedRaster:
|
|
379
|
+
"""Return classification snow and ice mask."""
|
|
380
|
+
logger.debug("read snow probability mask for %s", str(self))
|
|
381
|
+
return self.metadata.snow_probability(
|
|
382
|
+
dst_grid=grid,
|
|
383
|
+
resampling=resampling,
|
|
384
|
+
from_resolution=from_resolution,
|
|
385
|
+
cached_read=cached_read,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
def read_scl(
|
|
389
|
+
self,
|
|
390
|
+
grid: Union[GridProtocol, Resolution] = Resolution["20m"],
|
|
391
|
+
cached_read: bool = False,
|
|
392
|
+
) -> ReferencedRaster:
|
|
393
|
+
"""Return SCL mask."""
|
|
394
|
+
grid = (
|
|
395
|
+
self.metadata.grid(grid)
|
|
396
|
+
if isinstance(grid, Resolution)
|
|
397
|
+
else Grid.from_obj(grid)
|
|
398
|
+
)
|
|
399
|
+
grid_hash = hash((grid.transform, grid.shape))
|
|
400
|
+
if grid_hash not in self._scl_cache:
|
|
401
|
+
logger.debug("read SCL mask for %s", str(self))
|
|
402
|
+
self._scl_cache[grid_hash] = read_mask_as_raster(
|
|
403
|
+
asset_mpath(self.item, "scl"),
|
|
404
|
+
dst_grid=grid,
|
|
405
|
+
resampling=Resampling.nearest,
|
|
406
|
+
masked=True,
|
|
407
|
+
cached_read=cached_read,
|
|
408
|
+
)
|
|
409
|
+
return self._scl_cache[grid_hash]
|
|
410
|
+
|
|
411
|
+
def footprint_nodata_mask(
|
|
412
|
+
self,
|
|
413
|
+
grid: Union[GridProtocol, Resolution] = Resolution["10m"],
|
|
414
|
+
buffer_m: float = 0,
|
|
415
|
+
) -> ReferencedRaster:
|
|
416
|
+
"""Return rasterized footprint mask."""
|
|
417
|
+
grid = (
|
|
418
|
+
self.metadata.grid(grid)
|
|
419
|
+
if isinstance(grid, Resolution)
|
|
420
|
+
else Grid.from_obj(grid)
|
|
421
|
+
)
|
|
422
|
+
if buffer_m:
|
|
423
|
+
footprint = buffer_antimeridian_safe(shape(self), buffer_m=buffer_m)
|
|
424
|
+
if footprint.is_empty:
|
|
425
|
+
raise EmptyFootprintException(
|
|
426
|
+
f"buffer value of {buffer_m} results in an empty geometry for footprint {shape(self).wkt}"
|
|
427
|
+
)
|
|
428
|
+
else:
|
|
429
|
+
footprint = shape(self)
|
|
430
|
+
|
|
431
|
+
return ReferencedRaster(
|
|
432
|
+
rasterize(
|
|
433
|
+
[
|
|
434
|
+
reproject_geometry(
|
|
435
|
+
footprint,
|
|
436
|
+
self.crs,
|
|
437
|
+
grid.crs,
|
|
438
|
+
# CRS Bounds are sometimes smaller than (Mapchete) Grid Bounds,
|
|
439
|
+
# if clipping allowed it will mask out features at CRS Bounds border,
|
|
440
|
+
# therefore clip_to_crs_bounds: False; see mapchete.geometry.reproject reproject_geometry
|
|
441
|
+
clip_to_crs_bounds=False,
|
|
442
|
+
)
|
|
443
|
+
],
|
|
444
|
+
out_shape=grid.shape,
|
|
445
|
+
transform=grid.transform,
|
|
446
|
+
all_touched=True,
|
|
447
|
+
fill=1,
|
|
448
|
+
default_value=0,
|
|
449
|
+
).astype(bool),
|
|
450
|
+
transform=grid.transform,
|
|
451
|
+
bounds=grid.bounds,
|
|
452
|
+
crs=grid.crs,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def get_mask(
|
|
456
|
+
self,
|
|
457
|
+
grid: Union[GridProtocol, Resolution] = Resolution["10m"],
|
|
458
|
+
mask_config: MaskConfig = MaskConfig(),
|
|
459
|
+
target_mask: Optional[np.ndarray] = None,
|
|
460
|
+
) -> ReferencedRaster:
|
|
461
|
+
"""Merge masks into one 2D array."""
|
|
462
|
+
grid = (
|
|
463
|
+
self.metadata.grid(grid)
|
|
464
|
+
if isinstance(grid, Resolution)
|
|
465
|
+
else Grid.from_obj(grid)
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if target_mask is None:
|
|
469
|
+
target_mask = np.zeros(shape=grid.shape, dtype=bool)
|
|
470
|
+
else:
|
|
471
|
+
if target_mask.shape != grid.shape:
|
|
472
|
+
raise ValueError("a target mask must have the same shape as the grid")
|
|
473
|
+
logger.debug("got custom target mask to start with: %s", target_mask)
|
|
474
|
+
|
|
475
|
+
def _check_full(arr):
|
|
476
|
+
# ATTENTION: target_mask and out have to be combined *after* mask was buffered!
|
|
477
|
+
# use 'logical or' not '+' !!!
|
|
478
|
+
if (arr | target_mask).all():
|
|
479
|
+
raise AllMasked()
|
|
480
|
+
|
|
481
|
+
out = np.zeros(shape=grid.shape, dtype=bool)
|
|
482
|
+
logger.debug("generate mask for product %s ...", str(self))
|
|
483
|
+
try:
|
|
484
|
+
_check_full(out)
|
|
485
|
+
if mask_config.footprint:
|
|
486
|
+
logger.debug("generate footprint nodata mask ...")
|
|
487
|
+
try:
|
|
488
|
+
out |= self.footprint_nodata_mask(
|
|
489
|
+
grid, buffer_m=mask_config.footprint_buffer_m
|
|
490
|
+
).data
|
|
491
|
+
_check_full(out)
|
|
492
|
+
except EmptyFootprintException:
|
|
493
|
+
raise AllMasked()
|
|
494
|
+
if mask_config.l1c_cloud_type:
|
|
495
|
+
logger.debug("generate L1C mask ...")
|
|
496
|
+
out |= self.read_l1c_cloud_mask(
|
|
497
|
+
grid,
|
|
498
|
+
mask_config.l1c_cloud_type,
|
|
499
|
+
cached_read=mask_config.l1c_cloud_mask_cached_read,
|
|
500
|
+
).data
|
|
501
|
+
_check_full(out)
|
|
502
|
+
if mask_config.cloud_probability_threshold != 100:
|
|
503
|
+
logger.debug(
|
|
504
|
+
"generate cloud probability (%s) mask ...",
|
|
505
|
+
mask_config.cloud_probability_threshold,
|
|
506
|
+
)
|
|
507
|
+
cld_prb = self.read_cloud_probability(
|
|
508
|
+
grid,
|
|
509
|
+
from_resolution=mask_config.cloud_probability_resolution,
|
|
510
|
+
cached_read=mask_config.cloud_probability_cached_read,
|
|
511
|
+
).data
|
|
512
|
+
out |= np.where(
|
|
513
|
+
cld_prb >= mask_config.cloud_probability_threshold, True, False
|
|
514
|
+
)
|
|
515
|
+
_check_full(out)
|
|
516
|
+
if mask_config.scl_classes:
|
|
517
|
+
logger.debug(
|
|
518
|
+
"generate SCL mask using %s ...",
|
|
519
|
+
", ".join(
|
|
520
|
+
[scl_class.name for scl_class in mask_config.scl_classes]
|
|
521
|
+
),
|
|
522
|
+
)
|
|
523
|
+
# convert SCL classes to pixel values
|
|
524
|
+
scl_values = [scl.value for scl in mask_config.scl_classes]
|
|
525
|
+
# read SCL mask
|
|
526
|
+
scl_arr = self.read_scl(
|
|
527
|
+
grid, cached_read=mask_config.scl_cached_read
|
|
528
|
+
).data
|
|
529
|
+
# mask out specific pixel values
|
|
530
|
+
out |= np.isin(scl_arr, scl_values)
|
|
531
|
+
_check_full(out)
|
|
532
|
+
if mask_config.snow_ice:
|
|
533
|
+
logger.debug("generate snow & ice mask ...")
|
|
534
|
+
out |= self.read_snow_ice_mask(
|
|
535
|
+
grid, cached_read=mask_config.snow_ice_mask_cached_read
|
|
536
|
+
).data
|
|
537
|
+
_check_full(out)
|
|
538
|
+
if mask_config.snow_probability_threshold != 100:
|
|
539
|
+
logger.debug(
|
|
540
|
+
"generate snow probability (%s) mask ...",
|
|
541
|
+
mask_config.snow_probability_threshold,
|
|
542
|
+
)
|
|
543
|
+
snw_prb = self.read_snow_probability(
|
|
544
|
+
grid,
|
|
545
|
+
from_resolution=mask_config.snow_probability_resolution,
|
|
546
|
+
cached_read=mask_config.snow_probability_cached_read,
|
|
547
|
+
).data
|
|
548
|
+
out |= np.where(
|
|
549
|
+
snw_prb >= mask_config.snow_probability_threshold, True, False
|
|
550
|
+
)
|
|
551
|
+
_check_full(out)
|
|
552
|
+
if mask_config.buffer:
|
|
553
|
+
logger.debug(
|
|
554
|
+
"apply buffer (%s) to combined mask ...", mask_config.buffer
|
|
555
|
+
)
|
|
556
|
+
out = buffer_array(array=out, buffer=mask_config.buffer)
|
|
557
|
+
_check_full(out)
|
|
558
|
+
except AllMasked:
|
|
559
|
+
logger.debug(
|
|
560
|
+
"mask for product %s already full, skip reading other masks", self.id
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# ATTENTION: target_mask and out have to be combined *after* mask was buffered!
|
|
564
|
+
# use 'logical or' not '+' !!!
|
|
565
|
+
return ReferencedRaster(
|
|
566
|
+
out | target_mask,
|
|
567
|
+
transform=grid.transform,
|
|
568
|
+
crs=grid.crs,
|
|
569
|
+
bounds=grid.bounds,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
def _apply_sentinel2_bandpass_adjustment(
|
|
573
|
+
self, uncorrected: ma.MaskedArray, assets: List[str], computing_dtype=np.float32
|
|
574
|
+
) -> ma.MaskedArray:
|
|
575
|
+
out_arr: ma.MaskedArray = ma.masked_array(
|
|
576
|
+
data=np.zeros(uncorrected.shape, uncorrected.dtype),
|
|
577
|
+
mask=uncorrected.mask.copy(),
|
|
578
|
+
fill_value=uncorrected.fill_value,
|
|
579
|
+
)
|
|
580
|
+
for band_idx, asset in enumerate(assets):
|
|
581
|
+
out_arr[band_idx] = apply_bandpass_adjustment(
|
|
582
|
+
uncorrected[band_idx],
|
|
583
|
+
item=self.item,
|
|
584
|
+
l2a_band=asset_name_to_l2a_band(self.item, asset),
|
|
585
|
+
computing_dtype=computing_dtype,
|
|
586
|
+
out_dtype=uncorrected.dtype,
|
|
587
|
+
)
|
|
588
|
+
return out_arr
|
|
589
|
+
|
|
590
|
+
def _apply_brdf(
|
|
591
|
+
self,
|
|
592
|
+
uncorrected: ma.MaskedArray,
|
|
593
|
+
assets: List[str],
|
|
594
|
+
brdf_config: BRDFConfig,
|
|
595
|
+
grid: Union[GridProtocol, Resolution, None] = Resolution["10m"],
|
|
596
|
+
resampling: Resampling = Resampling.nearest,
|
|
597
|
+
mask_config: MaskConfig = MaskConfig(),
|
|
598
|
+
) -> ma.MaskedArray:
|
|
599
|
+
out_arr: ma.MaskedArray = ma.masked_array(
|
|
600
|
+
data=np.zeros(uncorrected.shape, uncorrected.dtype),
|
|
601
|
+
mask=uncorrected.mask.copy(),
|
|
602
|
+
fill_value=uncorrected.fill_value,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
# apply default correction defined in root
|
|
606
|
+
if brdf_config.model == BRDFModels.none:
|
|
607
|
+
logger.debug("no default BRDF model specified")
|
|
608
|
+
out_arr[:] = uncorrected
|
|
609
|
+
else:
|
|
610
|
+
logger.debug("applying %s to bands", brdf_config.model)
|
|
611
|
+
for band_idx, asset in enumerate(assets):
|
|
612
|
+
out_arr[band_idx] = apply_correction(
|
|
613
|
+
band=uncorrected[band_idx],
|
|
614
|
+
correction=self.read_brdf_grid(
|
|
615
|
+
asset_name_to_l2a_band(self.item, asset),
|
|
616
|
+
resampling=resampling,
|
|
617
|
+
grid=grid,
|
|
618
|
+
brdf_config=brdf_config,
|
|
619
|
+
),
|
|
620
|
+
correction_weight=brdf_config.correction_weight,
|
|
621
|
+
log10_bands_scale=brdf_config.log10_bands_scale,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# if SCL-specific correction is configured, apply and overwrite values in array
|
|
625
|
+
if brdf_config.scl_specific_configurations:
|
|
626
|
+
logger.debug("SCL class specific BRDF correction required")
|
|
627
|
+
scl_arr = self.read_scl(grid, mask_config.scl_cached_read).data
|
|
628
|
+
|
|
629
|
+
for scl_config in brdf_config.scl_specific_configurations:
|
|
630
|
+
scl_mask = np.isin(
|
|
631
|
+
scl_arr, [scl_class.value for scl_class in scl_config.scl_classes]
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
for band_idx, asset in enumerate(assets):
|
|
635
|
+
if scl_config.model == BRDFModels.none:
|
|
636
|
+
# use uncorrected values from original array
|
|
637
|
+
out_arr[band_idx][scl_mask] = uncorrected[band_idx][scl_mask]
|
|
638
|
+
|
|
639
|
+
elif scl_mask.any():
|
|
640
|
+
logger.debug(
|
|
641
|
+
"applying BRDF model %s to SCL classes %s",
|
|
642
|
+
scl_config.model.value,
|
|
643
|
+
", ".join(
|
|
644
|
+
[scl_class.name for scl_class in scl_config.scl_classes]
|
|
645
|
+
),
|
|
646
|
+
)
|
|
647
|
+
# apply correction band by band
|
|
648
|
+
out_arr[band_idx][scl_mask] = apply_correction(
|
|
649
|
+
uncorrected[band_idx],
|
|
650
|
+
self.read_brdf_grid(
|
|
651
|
+
asset_name_to_l2a_band(self.item, asset),
|
|
652
|
+
resampling=resampling,
|
|
653
|
+
grid=grid,
|
|
654
|
+
brdf_config=scl_config,
|
|
655
|
+
),
|
|
656
|
+
correction_weight=scl_config.correction_weight,
|
|
657
|
+
log10_bands_scale=scl_config.log10_bands_scale,
|
|
658
|
+
)[scl_mask]
|
|
659
|
+
|
|
660
|
+
# leave it be for all other cases
|
|
661
|
+
|
|
662
|
+
return out_arr
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def asset_name_to_l2a_band(item: pystac.Item, asset_name: str) -> L2ABand:
|
|
666
|
+
asset = item.assets[asset_name]
|
|
667
|
+
asset_path = MPath(asset.href)
|
|
668
|
+
band_name = asset_path.name.split(".")[0]
|
|
669
|
+
return L2ABand[band_name]
|