mapchete-eo 2026.2.0__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 (89) hide show
  1. mapchete_eo/__init__.py +1 -0
  2. mapchete_eo/array/__init__.py +0 -0
  3. mapchete_eo/array/buffer.py +16 -0
  4. mapchete_eo/array/color.py +29 -0
  5. mapchete_eo/array/convert.py +163 -0
  6. mapchete_eo/base.py +653 -0
  7. mapchete_eo/blacklist.txt +175 -0
  8. mapchete_eo/cli/__init__.py +30 -0
  9. mapchete_eo/cli/bounds.py +22 -0
  10. mapchete_eo/cli/options_arguments.py +227 -0
  11. mapchete_eo/cli/s2_brdf.py +77 -0
  12. mapchete_eo/cli/s2_cat_results.py +130 -0
  13. mapchete_eo/cli/s2_find_broken_products.py +77 -0
  14. mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
  15. mapchete_eo/cli/s2_mask.py +71 -0
  16. mapchete_eo/cli/s2_mgrs.py +45 -0
  17. mapchete_eo/cli/s2_rgb.py +114 -0
  18. mapchete_eo/cli/s2_verify.py +129 -0
  19. mapchete_eo/cli/static_catalog.py +82 -0
  20. mapchete_eo/eostac.py +30 -0
  21. mapchete_eo/exceptions.py +87 -0
  22. mapchete_eo/image_operations/__init__.py +12 -0
  23. mapchete_eo/image_operations/blend_functions.py +579 -0
  24. mapchete_eo/image_operations/color_correction.py +136 -0
  25. mapchete_eo/image_operations/compositing.py +266 -0
  26. mapchete_eo/image_operations/dtype_scale.py +43 -0
  27. mapchete_eo/image_operations/fillnodata.py +130 -0
  28. mapchete_eo/image_operations/filters.py +319 -0
  29. mapchete_eo/image_operations/linear_normalization.py +81 -0
  30. mapchete_eo/image_operations/sigmoidal.py +114 -0
  31. mapchete_eo/io/__init__.py +37 -0
  32. mapchete_eo/io/assets.py +496 -0
  33. mapchete_eo/io/items.py +162 -0
  34. mapchete_eo/io/levelled_cubes.py +259 -0
  35. mapchete_eo/io/path.py +155 -0
  36. mapchete_eo/io/products.py +423 -0
  37. mapchete_eo/io/profiles.py +45 -0
  38. mapchete_eo/platforms/sentinel2/__init__.py +17 -0
  39. mapchete_eo/platforms/sentinel2/_mapper_registry.py +89 -0
  40. mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
  41. mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
  42. mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
  43. mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
  44. mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
  45. mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
  46. mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
  47. mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
  48. mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
  49. mapchete_eo/platforms/sentinel2/config.py +241 -0
  50. mapchete_eo/platforms/sentinel2/driver.py +43 -0
  51. mapchete_eo/platforms/sentinel2/masks.py +329 -0
  52. mapchete_eo/platforms/sentinel2/metadata_parser/__init__.py +6 -0
  53. mapchete_eo/platforms/sentinel2/metadata_parser/base.py +56 -0
  54. mapchete_eo/platforms/sentinel2/metadata_parser/default_path_mapper.py +135 -0
  55. mapchete_eo/platforms/sentinel2/metadata_parser/models.py +78 -0
  56. mapchete_eo/platforms/sentinel2/metadata_parser/s2metadata.py +639 -0
  57. mapchete_eo/platforms/sentinel2/preconfigured_sources/__init__.py +57 -0
  58. mapchete_eo/platforms/sentinel2/preconfigured_sources/guessers.py +108 -0
  59. mapchete_eo/platforms/sentinel2/preconfigured_sources/item_mappers.py +171 -0
  60. mapchete_eo/platforms/sentinel2/preconfigured_sources/metadata_xml_mappers.py +217 -0
  61. mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +50 -0
  62. mapchete_eo/platforms/sentinel2/processing_baseline.py +163 -0
  63. mapchete_eo/platforms/sentinel2/product.py +747 -0
  64. mapchete_eo/platforms/sentinel2/source.py +114 -0
  65. mapchete_eo/platforms/sentinel2/types.py +114 -0
  66. mapchete_eo/processes/__init__.py +0 -0
  67. mapchete_eo/processes/config.py +51 -0
  68. mapchete_eo/processes/dtype_scale.py +112 -0
  69. mapchete_eo/processes/eo_to_xarray.py +19 -0
  70. mapchete_eo/processes/merge_rasters.py +239 -0
  71. mapchete_eo/product.py +323 -0
  72. mapchete_eo/protocols.py +61 -0
  73. mapchete_eo/search/__init__.py +14 -0
  74. mapchete_eo/search/base.py +285 -0
  75. mapchete_eo/search/config.py +113 -0
  76. mapchete_eo/search/s2_mgrs.py +313 -0
  77. mapchete_eo/search/stac_search.py +278 -0
  78. mapchete_eo/search/stac_static.py +197 -0
  79. mapchete_eo/search/utm_search.py +251 -0
  80. mapchete_eo/settings.py +25 -0
  81. mapchete_eo/sort.py +60 -0
  82. mapchete_eo/source.py +109 -0
  83. mapchete_eo/time.py +62 -0
  84. mapchete_eo/types.py +76 -0
  85. mapchete_eo-2026.2.0.dist-info/METADATA +91 -0
  86. mapchete_eo-2026.2.0.dist-info/RECORD +89 -0
  87. mapchete_eo-2026.2.0.dist-info/WHEEL +4 -0
  88. mapchete_eo-2026.2.0.dist-info/entry_points.txt +11 -0
  89. mapchete_eo-2026.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,32 @@
1
+ from enum import Enum
2
+ from typing import NamedTuple
3
+
4
+
5
+ class BRDFModels(str, Enum):
6
+ none = "none"
7
+ HLS = "HLS"
8
+ RossThick = "RossThick"
9
+
10
+
11
+ class ModelParameters(NamedTuple):
12
+ f_iso: float
13
+ f_geo: float
14
+ f_vol: float
15
+
16
+
17
+ # Source for bands outside of RGBNIR range:
18
+ # https://www.sciencedirect.com/science/article/pii/S0034425717302791
19
+ # https://www.semanticscholar.org/paper/Adjustment-of-Sentinel-2-Multi-Spectral-Instrument-Roy-Li/be90a03a19c612763f966fae5290222a4b76bba6
20
+ class L2ABandFParams(ModelParameters, Enum):
21
+ B01 = ModelParameters(0.0774, 0.0079, 0.0372)
22
+ B02 = ModelParameters(0.0774, 0.0079, 0.0372)
23
+ B03 = ModelParameters(0.1306, 0.0178, 0.0580)
24
+ B04 = ModelParameters(0.1690, 0.0227, 0.0574)
25
+ B05 = ModelParameters(0.2085, 0.0256, 0.0845)
26
+ B06 = ModelParameters(0.2316, 0.0273, 0.1003)
27
+ B07 = ModelParameters(0.2599, 0.0294, 0.1197)
28
+ B08 = ModelParameters(0.3093, 0.0330, 0.1535)
29
+ B8A = ModelParameters(0.3093, 0.0330, 0.1535)
30
+ B09 = ModelParameters(0.3201, 0.0471, 0.1611)
31
+ B11 = ModelParameters(0.3430, 0.0453, 0.1154)
32
+ B12 = ModelParameters(0.2658, 0.0387, 0.0639)
@@ -0,0 +1,260 @@
1
+ import logging
2
+ from typing import List
3
+
4
+ from mapchete import Timer
5
+ from mapchete.io.raster import ReferencedRaster, resample_from_array
6
+ from mapchete.protocols import GridProtocol
7
+ from mapchete.types import NodataVal
8
+ import numpy as np
9
+ import numpy.ma as ma
10
+ from numpy.typing import DTypeLike
11
+ from rasterio.enums import Resampling
12
+ from rasterio.fill import fillnodata
13
+
14
+ from mapchete_eo.exceptions import BRDFError
15
+ from mapchete_eo.platforms.sentinel2.brdf.models import BRDFModels, get_model
16
+ from mapchete_eo.platforms.sentinel2.metadata_parser.s2metadata import S2Metadata
17
+ from mapchete_eo.platforms.sentinel2.types import (
18
+ L2ABand,
19
+ Resolution,
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def _correction_combine_detectors(
26
+ s2_metadata: S2Metadata,
27
+ band: L2ABand,
28
+ out_grid: GridProtocol,
29
+ model: BRDFModels = BRDFModels.HLS,
30
+ dtype: DTypeLike = np.float32,
31
+ ) -> ma.MaskedArray:
32
+ """
33
+ Run correction using combined angle masks of all
34
+ """
35
+ return resample_from_array(
36
+ get_model(
37
+ model=model, s2_metadata=s2_metadata, band=band, processing_dtype=dtype
38
+ ).calculate(),
39
+ out_grid=out_grid,
40
+ nodata=0,
41
+ resampling=Resampling.bilinear,
42
+ keep_2d=True,
43
+ )
44
+
45
+
46
+ def _correction_per_detector(
47
+ s2_metadata: S2Metadata,
48
+ band: L2ABand,
49
+ out_grid: GridProtocol,
50
+ model: BRDFModels = BRDFModels.HLS,
51
+ smoothing_iterations: int = 10,
52
+ dtype: DTypeLike = np.float32,
53
+ footprints_cached_read: bool = True,
54
+ ) -> ma.MaskedArray:
55
+ """
56
+ Run correction separately for each detector footprint.
57
+ """
58
+ # create output array
59
+ model_params = ma.masked_equal(np.zeros(out_grid.shape, dtype=dtype), 0)
60
+
61
+ # get detector footprints
62
+ detector_footprints = s2_metadata.detector_footprints(
63
+ band, cached_read=footprints_cached_read
64
+ )
65
+ resampled_detector_footprints = resample_from_array(
66
+ detector_footprints,
67
+ out_grid=out_grid,
68
+ nodata=0,
69
+ resampling=Resampling.nearest,
70
+ keep_2d=True,
71
+ )
72
+ if resampled_detector_footprints.ndim not in [2, 3]:
73
+ raise ValueError(
74
+ f"detector_footprints has to be a 2- or 3-dimensional array but has shape {detector_footprints.shape}"
75
+ )
76
+ if resampled_detector_footprints.ndim == 3:
77
+ resampled_detector_footprints = resampled_detector_footprints[0]
78
+
79
+ # determine available detector IDs
80
+ detector_ids: List[int] = [
81
+ detector_id
82
+ for detector_id in np.unique(resampled_detector_footprints)
83
+ if detector_id != 0
84
+ ]
85
+
86
+ # get viewing angle arrays per detector
87
+ viewing_azimuth_per_detector = s2_metadata.viewing_incidence_angles(
88
+ band
89
+ ).azimuth.detectors
90
+ viewing_zenith_per_detector = s2_metadata.viewing_incidence_angles(
91
+ band
92
+ ).zenith.detectors
93
+
94
+ # iterate through detector footprints and calculate BRDF for each one
95
+ for detector_id in detector_ids:
96
+ logger.debug("run on detector %s", detector_id)
97
+
98
+ # handle rare cases where detector geometries are available but no respective
99
+ # angle arrays:
100
+ if detector_id not in viewing_zenith_per_detector: # pragma: no cover
101
+ logger.debug("no zenith angles grid found for detector %s", detector_id)
102
+ continue
103
+ if detector_id not in viewing_azimuth_per_detector: # pragma: no cover
104
+ logger.debug("no azimuth angles grid found for detector %s", detector_id)
105
+ continue
106
+
107
+ # select pixels which are covered by detector
108
+ detector_mask = np.where(
109
+ resampled_detector_footprints == detector_id, True, False
110
+ )
111
+
112
+ # skip if detector footprint does not intersect with output window
113
+ if not detector_mask.any(): # pragma: no cover
114
+ logger.debug("detector %s does not intersect with band window", detector_id)
115
+ continue
116
+
117
+ # run low resolution model
118
+ model_values = get_model(
119
+ model=model,
120
+ s2_metadata=s2_metadata,
121
+ band=band,
122
+ detector_id=detector_id,
123
+ processing_dtype=dtype,
124
+ ).calculate()
125
+
126
+ # interpolate missing nodata edges and return BRDF difference model
127
+ detector_brdf_param = ma.masked_invalid(
128
+ fillnodata(model_values.data, smoothing_iterations=smoothing_iterations)
129
+ )
130
+
131
+ # resample model to output resolution
132
+ detector_brdf = resample_from_array(
133
+ detector_brdf_param,
134
+ out_grid=out_grid,
135
+ array_transform=model_values.transform,
136
+ in_crs=model_values.crs,
137
+ nodata=0,
138
+ resampling=Resampling.bilinear,
139
+ keep_2d=True,
140
+ )
141
+ # merge detector stripes
142
+ model_params[detector_mask] = detector_brdf[detector_mask]
143
+ model_params.mask[detector_mask] = detector_brdf.mask[detector_mask]
144
+
145
+ return model_params
146
+
147
+
148
+ def correction_values(
149
+ s2_metadata: S2Metadata,
150
+ band: L2ABand,
151
+ model: BRDFModels = BRDFModels.HLS,
152
+ resolution: Resolution = Resolution["60m"],
153
+ footprints_cached_read: bool = False,
154
+ per_detector: bool = True,
155
+ dtype: DTypeLike = np.float32,
156
+ ) -> ReferencedRaster:
157
+ """Calculate BRDF correction values.
158
+
159
+ Calculation is always done on original product CRS, but the resolution
160
+ can be defined.
161
+ """
162
+ with Timer() as t:
163
+ if per_detector:
164
+ # Per Detector strategy:
165
+ brdf_params = _correction_per_detector(
166
+ s2_metadata=s2_metadata,
167
+ band=band,
168
+ out_grid=s2_metadata.grid(resolution),
169
+ model=model,
170
+ dtype=dtype,
171
+ footprints_cached_read=footprints_cached_read,
172
+ )
173
+ else:
174
+ brdf_params = _correction_combine_detectors(
175
+ s2_metadata=s2_metadata,
176
+ band=band,
177
+ out_grid=s2_metadata.grid(resolution),
178
+ model=model,
179
+ dtype=dtype,
180
+ )
181
+ logger.debug(
182
+ f"BRDF for product {s2_metadata.product_id} band {band.name} calculated in {str(t)}"
183
+ )
184
+ if brdf_params.mask.all(): # pragma: no cover
185
+ raise BRDFError(f"BRDF grid array for {s2_metadata.product_id} is empty!")
186
+ return ReferencedRaster(
187
+ data=brdf_params,
188
+ transform=s2_metadata.transform(resolution),
189
+ crs=s2_metadata.crs,
190
+ bounds=s2_metadata.bounds,
191
+ driver="COG",
192
+ )
193
+
194
+
195
+ def apply_correction(
196
+ band: ma.MaskedArray,
197
+ correction: np.ndarray,
198
+ log10_bands_scale: bool = False,
199
+ correction_weight: float = 1.0,
200
+ nodata: NodataVal = 0,
201
+ ) -> ma.MaskedArray:
202
+ """
203
+ Apply BRDF parameter to band.
204
+
205
+ If target nodata value is 0, then the corrected band values that would become 0 are
206
+ set to 1.
207
+
208
+ Parameters
209
+ ----------
210
+ band : numpy.ma.MaskedArray
211
+ brdf_param : numpy.ma.MaskedArray
212
+ nodata : nodata value used to mask output
213
+
214
+ Returns
215
+ -------
216
+ BRDF corrected band : numpy.ma.MaskedArray
217
+ """
218
+ if isinstance(band, ma.MaskedArray) and band.mask.all(): # pragma: no cover
219
+ return band
220
+ else:
221
+ mask = (
222
+ band.mask
223
+ if isinstance(band, ma.MaskedArray)
224
+ else np.where(band == nodata, True, False)
225
+ )
226
+
227
+ if correction_weight != 1.0:
228
+ logger.debug("apply weight to correction")
229
+ # a correction_weight value of >1 should increase the correction, whereas a
230
+ # value <1 should decrease the correction
231
+ correction = 1 - (1 - correction) * correction_weight
232
+
233
+ if log10_bands_scale:
234
+ # # Apply BRDF correction to log10 scaled Sentinel-2 data
235
+ corrected = (
236
+ np.log10(band.astype(np.float32, copy=False), where=band > 0)
237
+ * correction
238
+ ).astype(np.float32, copy=False)
239
+ # Revert the log to linear
240
+ corrected = (np.power(10, corrected)).astype(np.float32, copy=False)
241
+ else:
242
+ corrected = (band.astype(np.float32, copy=False) * correction).astype(
243
+ band.dtype, copy=False
244
+ )
245
+
246
+ if nodata == 0:
247
+ return ma.masked_array(
248
+ data=np.where(
249
+ mask,
250
+ 0,
251
+ np.clip(corrected, 1, np.iinfo(band.dtype).max).astype(
252
+ band.dtype, copy=False
253
+ ),
254
+ ),
255
+ mask=mask,
256
+ )
257
+ else: # pragma: no cover
258
+ return ma.masked_array(
259
+ data=corrected.astype(band.dtype, copy=False), mask=mask
260
+ )
@@ -0,0 +1,251 @@
1
+ """
2
+ Legacy implementation from before 2024.
3
+ """
4
+
5
+ from __future__ import annotations
6
+ from typing import Optional, Tuple
7
+
8
+ from affine import Affine
9
+ from mapchete.io.raster import ReferencedRaster
10
+ from mapchete.types import CRSLike
11
+ import numpy as np
12
+ from numpy.typing import DTypeLike
13
+
14
+ from mapchete_eo.platforms.sentinel2.brdf.protocols import (
15
+ BRDFModelProtocol,
16
+ )
17
+ from mapchete_eo.platforms.sentinel2.brdf.config import L2ABandFParams, ModelParameters
18
+ from mapchete_eo.platforms.sentinel2.brdf.sun_angle_arrays import get_sun_zenith_angles
19
+ from mapchete_eo.platforms.sentinel2.metadata_parser.s2metadata import S2Metadata
20
+ from mapchete_eo.platforms.sentinel2.types import L2ABand
21
+
22
+
23
+ class HLSBaseModel:
24
+ """Base class for sensor and sun models."""
25
+
26
+ # Class with adapted Sentinel-2 Sentinel-Hub Normalization (Also used elsewhere)
27
+ # Sources:
28
+ # https://sci-hub.st/https://ieeexplore.ieee.org/document/8899868
29
+ # https://sci-hub.st/https://ieeexplore.ieee.org/document/841980
30
+ # https://custom-scripts.sentinel-hub.com/sentinel-2/brdf/
31
+ # Alt GitHub: https://github.com/maximlamare/s2-normalisation
32
+ sun_zenith_radian: np.ndarray
33
+ sun_azimuth_radian: np.ndarray
34
+ view_zenith_radian: np.ndarray
35
+ view_azimuth_radian: np.ndarray
36
+ f_band_params: ModelParameters
37
+ relative_azimuth_angle_radian: np.ndarray
38
+ processing_dtype: DTypeLike = np.float32
39
+
40
+ def __init__(
41
+ self,
42
+ sun_zenith_radian: np.ndarray,
43
+ sun_azimuth_radian: np.ndarray,
44
+ view_zenith_radian: np.ndarray,
45
+ view_azimuth_radian: np.ndarray,
46
+ f_band_params: ModelParameters,
47
+ relative_azimuth_angle_radian: Optional[np.ndarray] = None,
48
+ processing_dtype: DTypeLike = np.float32,
49
+ ):
50
+ self.sun_zenith_radian = sun_zenith_radian
51
+ self.sun_azimuth_radian = sun_azimuth_radian
52
+ self.view_zenith_radian = view_zenith_radian
53
+ self.view_azimuth_radian = view_azimuth_radian
54
+ self.f_band_params = f_band_params
55
+ self.processing_dtype = processing_dtype
56
+
57
+ # relative azimuth angle (in rad)
58
+ if relative_azimuth_angle_radian is None:
59
+ _phi = np.deg2rad(
60
+ np.rad2deg(sun_azimuth_radian) - np.rad2deg(view_azimuth_radian)
61
+ )
62
+ self.relative_azimuth_angle_radian = np.where(
63
+ _phi < 0, _phi + 2 * np.pi, _phi
64
+ )
65
+
66
+ else:
67
+ self.relative_azimuth_angle_radian = relative_azimuth_angle_radian
68
+
69
+ # Get delta
70
+ def delta(self):
71
+ return np.sqrt(
72
+ np.power(np.tan(self.sun_zenith_radian), 2)
73
+ + np.power(np.tan(self.view_zenith_radian), 2)
74
+ - 2
75
+ * np.tan(self.sun_zenith_radian)
76
+ * np.tan(self.view_zenith_radian)
77
+ * np.cos(self.relative_azimuth_angle_radian)
78
+ )
79
+
80
+ # Air Mass
81
+ def masse(self):
82
+ return 1 / np.cos(self.sun_zenith_radian) + 1 / np.cos(self.view_zenith_radian)
83
+
84
+ # Get xsi
85
+ def cos_xsi(self):
86
+ return np.cos(self.sun_zenith_radian) * np.cos(
87
+ self.view_zenith_radian
88
+ ) + np.sin(self.sun_zenith_radian) * np.sin(self.view_zenith_radian) * np.cos(
89
+ self.relative_azimuth_angle_radian
90
+ )
91
+
92
+ def sin_xsi(self):
93
+ return np.sqrt(1 - np.power(self.cos_xsi(), 2))
94
+
95
+ def xsi(self):
96
+ xsi = np.arccos(self.cos_xsi())
97
+ return xsi
98
+
99
+ # Function t
100
+ def cos_t(self):
101
+ trig = (
102
+ np.tan(self.sun_zenith_radian)
103
+ * np.tan(self.view_zenith_radian)
104
+ * np.sin(self.relative_azimuth_angle_radian)
105
+ )
106
+ # Coeficient for "t" any natural number is good, 1 or 2 are used
107
+ coef = 1
108
+ cos_t = (
109
+ coef / self.masse() * np.sqrt(np.power(self.delta(), 2) + np.power(trig, 2))
110
+ )
111
+ return np.clip(cos_t, -1, 1)
112
+
113
+ def sin_t(self):
114
+ return np.sqrt(1 - np.power(self.cos_t(), 2))
115
+
116
+ def t(self):
117
+ return np.arccos(self.cos_t())
118
+
119
+ def sec(self, x: np.ndarray) -> np.ndarray:
120
+ return 1 / np.cos(x)
121
+
122
+ # Function FV Ross_Thick, V is for volume scattering (Kernel)
123
+ def f_vol(self):
124
+ return (self.masse() / np.pi) * (
125
+ (self.t() - self.sin_t() * self.cos_t() - np.pi)
126
+ + (
127
+ (1 + self.cos_xsi())
128
+ / (2 * np.cos(self.sun_zenith_radian) * np.cos(self.view_zenith_radian))
129
+ )
130
+ )
131
+
132
+ # Function FR Li-Sparse, R is for roughness (surface roughness)
133
+ def f_roughness(self):
134
+ # HLS formula
135
+ # https://userpages.umbc.edu/~martins/PHYS650/maignan%20brdf.pdf
136
+ a = 1 / (np.cos(self.sun_zenith_radian) + np.cos(self.view_zenith_radian))
137
+ return 4 / (3 * np.pi) * a * (
138
+ (np.pi / 2 - self.xsi()) * self.cos_xsi() + self.sin_xsi()
139
+ ) - (1 / 3)
140
+
141
+ def calculate_array(self) -> np.ndarray:
142
+ return (
143
+ self.f_band_params.f_iso
144
+ + self.f_band_params.f_geo * self.f_roughness()
145
+ + self.f_band_params.f_vol * self.f_vol()
146
+ )
147
+
148
+
149
+ class HLS(BRDFModelProtocol):
150
+ """Directional model."""
151
+
152
+ sun_zenith: np.ndarray
153
+ sun_azimuth: np.ndarray
154
+ view_zenith: np.ndarray
155
+ view_azimuth: np.ndarray
156
+ f_band_params: ModelParameters
157
+ processing_dtype: DTypeLike = np.float32
158
+ transform: Affine
159
+ crs: CRSLike
160
+
161
+ def __init__(
162
+ self,
163
+ s2_metadata: S2Metadata,
164
+ band: L2ABand,
165
+ detector_id: Optional[int] = None,
166
+ processing_dtype: DTypeLike = np.float32,
167
+ ):
168
+ self.sun_zenith = s2_metadata.sun_angles.zenith.raster.data
169
+ self.sun_azimuth = s2_metadata.sun_angles.azimuth.raster.data
170
+ self.view_zenith, self.view_azimuth = _get_viewing_angles(
171
+ s2_metadata=s2_metadata, band=band, detector_id=detector_id
172
+ )
173
+ self.f_band_params = L2ABandFParams[band.name].value
174
+ self.processing_dtype = processing_dtype
175
+ self.sun_zenith_angles_radian = get_sun_zenith_angles(s2_metadata)
176
+ self.transform = s2_metadata.sun_angles.zenith.raster.transform
177
+ self.crs = s2_metadata.crs
178
+
179
+ def sensor_model(self) -> HLSBaseModel:
180
+ return HLSBaseModel(
181
+ sun_zenith_radian=np.deg2rad(self.sun_zenith),
182
+ sun_azimuth_radian=np.deg2rad(self.sun_azimuth),
183
+ view_zenith_radian=np.deg2rad(self.view_zenith),
184
+ view_azimuth_radian=np.deg2rad(self.view_azimuth),
185
+ f_band_params=self.f_band_params,
186
+ processing_dtype=self.processing_dtype,
187
+ )
188
+
189
+ def sun_model(self) -> HLSBaseModel:
190
+ # like sensor model, but:
191
+ # sun_zenith_radian = calculated sun zenith angles
192
+ # view_zenith_radian = np.zeros(self.sun_zenith_radian.shape)
193
+ # phi = np.zeros(self.sun_zenith_radian.shape)
194
+ return HLSBaseModel(
195
+ sun_zenith_radian=self.sun_zenith_angles_radian,
196
+ sun_azimuth_radian=np.deg2rad(self.sun_azimuth),
197
+ view_zenith_radian=np.zeros(self.sun_zenith_angles_radian.shape),
198
+ view_azimuth_radian=np.deg2rad(self.view_azimuth),
199
+ relative_azimuth_angle_radian=np.zeros(self.sun_zenith_angles_radian.shape),
200
+ f_band_params=self.f_band_params,
201
+ processing_dtype=self.processing_dtype,
202
+ )
203
+
204
+ def calculate(self) -> ReferencedRaster:
205
+ return ReferencedRaster.from_array_like(
206
+ array_like=(
207
+ self.sun_model().calculate_array()
208
+ / self.sensor_model().calculate_array()
209
+ ),
210
+ transform=self.transform,
211
+ crs=self.crs,
212
+ )
213
+
214
+ @staticmethod
215
+ def from_s2metadata(
216
+ s2_metadata: S2Metadata,
217
+ band: L2ABand,
218
+ detector_id: Optional[int] = None,
219
+ processing_dtype: DTypeLike = np.float32,
220
+ ) -> HLS:
221
+ return HLS(
222
+ s2_metadata=s2_metadata,
223
+ band=band,
224
+ detector_id=detector_id,
225
+ processing_dtype=processing_dtype,
226
+ )
227
+
228
+
229
+ def _get_viewing_angles(
230
+ s2_metadata: S2Metadata, band: L2ABand, detector_id: Optional[int] = None
231
+ ) -> Tuple[np.ndarray, np.ndarray]:
232
+ """Get viewing angles for single detector or for all detectors."""
233
+ if detector_id is not None:
234
+ view_zenith = (
235
+ s2_metadata.viewing_incidence_angles(band)
236
+ .zenith.detectors[detector_id]
237
+ .data
238
+ )
239
+ view_azimuth = (
240
+ s2_metadata.viewing_incidence_angles(band)
241
+ .azimuth.detectors[detector_id]
242
+ .data
243
+ )
244
+ else:
245
+ view_zenith = (
246
+ s2_metadata.viewing_incidence_angles(band).zenith.merge_detectors().data
247
+ )
248
+ view_azimuth = (
249
+ s2_metadata.viewing_incidence_angles(band).azimuth.merge_detectors().data
250
+ )
251
+ return (view_zenith, view_azimuth)
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Optional
5
+
6
+ import numpy as np
7
+ from numpy.typing import DTypeLike
8
+
9
+ from mapchete_eo.platforms.sentinel2.brdf.protocols import BRDFModelProtocol
10
+ from mapchete_eo.platforms.sentinel2.brdf.config import BRDFModels
11
+ from mapchete_eo.platforms.sentinel2.brdf.hls import HLS
12
+ from mapchete_eo.platforms.sentinel2.brdf.ross_thick import RossThick
13
+
14
+ # from mapchete_eo.platforms.sentinel2.brdf.hls2 import HLS2
15
+ from mapchete_eo.platforms.sentinel2.metadata_parser.s2metadata import S2Metadata
16
+ from mapchete_eo.platforms.sentinel2.types import L2ABand
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def get_model(
22
+ model: BRDFModels,
23
+ s2_metadata: S2Metadata,
24
+ band: L2ABand,
25
+ detector_id: Optional[int] = None,
26
+ processing_dtype: DTypeLike = np.float32,
27
+ ) -> BRDFModelProtocol:
28
+ match model:
29
+ case BRDFModels.HLS:
30
+ return HLS.from_s2metadata(
31
+ s2_metadata=s2_metadata,
32
+ band=band,
33
+ detector_id=detector_id,
34
+ processing_dtype=processing_dtype,
35
+ )
36
+ case BRDFModels.RossThick:
37
+ return RossThick.from_s2metadata(
38
+ s2_metadata=s2_metadata,
39
+ band=band,
40
+ detector_id=detector_id,
41
+ processing_dtype=processing_dtype,
42
+ )
43
+ case _:
44
+ raise KeyError(f"unkown or not implemented model: {model}")
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+ from typing import Optional, Protocol
3
+
4
+ from mapchete.io.raster import ReferencedRaster
5
+
6
+ import numpy as np
7
+ from numpy.typing import DTypeLike
8
+
9
+ from mapchete_eo.platforms.sentinel2.metadata_parser.s2metadata import S2Metadata
10
+ from mapchete_eo.platforms.sentinel2.types import L2ABand
11
+
12
+
13
+ class BRDFModelProtocol(Protocol):
14
+ """Defines base interface to all kind of models.
15
+
16
+ Can be sensor models, sun models or directional models!
17
+ """
18
+
19
+ def calculate(self) -> ReferencedRaster: ...
20
+
21
+ @staticmethod
22
+ def from_s2metadata(
23
+ s2_metadata: S2Metadata,
24
+ band: L2ABand,
25
+ detector_id: Optional[int] = None,
26
+ processing_dtype: DTypeLike = np.float32,
27
+ ) -> BRDFModelProtocol: ...