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.
Files changed (87) hide show
  1. mapchete_eo/__init__.py +1 -0
  2. mapchete_eo/archives/__init__.py +0 -0
  3. mapchete_eo/archives/base.py +65 -0
  4. mapchete_eo/array/__init__.py +0 -0
  5. mapchete_eo/array/buffer.py +16 -0
  6. mapchete_eo/array/color.py +29 -0
  7. mapchete_eo/array/convert.py +157 -0
  8. mapchete_eo/base.py +528 -0
  9. mapchete_eo/blacklist.txt +175 -0
  10. mapchete_eo/cli/__init__.py +30 -0
  11. mapchete_eo/cli/bounds.py +22 -0
  12. mapchete_eo/cli/options_arguments.py +243 -0
  13. mapchete_eo/cli/s2_brdf.py +77 -0
  14. mapchete_eo/cli/s2_cat_results.py +146 -0
  15. mapchete_eo/cli/s2_find_broken_products.py +93 -0
  16. mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
  17. mapchete_eo/cli/s2_mask.py +71 -0
  18. mapchete_eo/cli/s2_mgrs.py +45 -0
  19. mapchete_eo/cli/s2_rgb.py +114 -0
  20. mapchete_eo/cli/s2_verify.py +129 -0
  21. mapchete_eo/cli/static_catalog.py +123 -0
  22. mapchete_eo/eostac.py +30 -0
  23. mapchete_eo/exceptions.py +87 -0
  24. mapchete_eo/geometry.py +271 -0
  25. mapchete_eo/image_operations/__init__.py +12 -0
  26. mapchete_eo/image_operations/color_correction.py +136 -0
  27. mapchete_eo/image_operations/compositing.py +247 -0
  28. mapchete_eo/image_operations/dtype_scale.py +43 -0
  29. mapchete_eo/image_operations/fillnodata.py +130 -0
  30. mapchete_eo/image_operations/filters.py +319 -0
  31. mapchete_eo/image_operations/linear_normalization.py +81 -0
  32. mapchete_eo/image_operations/sigmoidal.py +114 -0
  33. mapchete_eo/io/__init__.py +37 -0
  34. mapchete_eo/io/assets.py +492 -0
  35. mapchete_eo/io/items.py +147 -0
  36. mapchete_eo/io/levelled_cubes.py +228 -0
  37. mapchete_eo/io/path.py +144 -0
  38. mapchete_eo/io/products.py +413 -0
  39. mapchete_eo/io/profiles.py +45 -0
  40. mapchete_eo/known_catalogs.py +42 -0
  41. mapchete_eo/platforms/sentinel2/__init__.py +17 -0
  42. mapchete_eo/platforms/sentinel2/archives.py +190 -0
  43. mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
  44. mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
  45. mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
  46. mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
  47. mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
  48. mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
  49. mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
  50. mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
  51. mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
  52. mapchete_eo/platforms/sentinel2/config.py +181 -0
  53. mapchete_eo/platforms/sentinel2/driver.py +78 -0
  54. mapchete_eo/platforms/sentinel2/masks.py +325 -0
  55. mapchete_eo/platforms/sentinel2/metadata_parser.py +734 -0
  56. mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +29 -0
  57. mapchete_eo/platforms/sentinel2/path_mappers/base.py +56 -0
  58. mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +34 -0
  59. mapchete_eo/platforms/sentinel2/path_mappers/metadata_xml.py +135 -0
  60. mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +105 -0
  61. mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +26 -0
  62. mapchete_eo/platforms/sentinel2/processing_baseline.py +160 -0
  63. mapchete_eo/platforms/sentinel2/product.py +669 -0
  64. mapchete_eo/platforms/sentinel2/types.py +109 -0
  65. mapchete_eo/processes/__init__.py +0 -0
  66. mapchete_eo/processes/config.py +51 -0
  67. mapchete_eo/processes/dtype_scale.py +112 -0
  68. mapchete_eo/processes/eo_to_xarray.py +19 -0
  69. mapchete_eo/processes/merge_rasters.py +235 -0
  70. mapchete_eo/product.py +278 -0
  71. mapchete_eo/protocols.py +56 -0
  72. mapchete_eo/search/__init__.py +14 -0
  73. mapchete_eo/search/base.py +222 -0
  74. mapchete_eo/search/config.py +42 -0
  75. mapchete_eo/search/s2_mgrs.py +314 -0
  76. mapchete_eo/search/stac_search.py +251 -0
  77. mapchete_eo/search/stac_static.py +236 -0
  78. mapchete_eo/search/utm_search.py +251 -0
  79. mapchete_eo/settings.py +24 -0
  80. mapchete_eo/sort.py +48 -0
  81. mapchete_eo/time.py +53 -0
  82. mapchete_eo/types.py +73 -0
  83. mapchete_eo-2025.7.0.dist-info/METADATA +38 -0
  84. mapchete_eo-2025.7.0.dist-info/RECORD +87 -0
  85. mapchete_eo-2025.7.0.dist-info/WHEEL +5 -0
  86. mapchete_eo-2025.7.0.dist-info/entry_points.txt +11 -0
  87. 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]