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,104 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import NamedTuple
|
|
3
|
+
import numpy as np
|
|
4
|
+
import numpy.ma as ma
|
|
5
|
+
from numpy.typing import DTypeLike
|
|
6
|
+
|
|
7
|
+
from pystac import Item
|
|
8
|
+
|
|
9
|
+
from mapchete_eo.platforms.sentinel2.types import L2ABand
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BandpassAdjustment(NamedTuple):
|
|
13
|
+
slope: float
|
|
14
|
+
intercept: float
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Bandpass Adjustment for Sentinel-2
|
|
18
|
+
# Try using HLS bandpass adjustmets
|
|
19
|
+
# https://hls.gsfc.nasa.gov/algorithms/bandpass-adjustment/
|
|
20
|
+
# https://lpdaac.usgs.gov/documents/1698/HLS_User_Guide_V2.pdf
|
|
21
|
+
# These are for Sentinel-2B bandpass adjustment; fisrt is slope second is intercept
|
|
22
|
+
# out_band = band * slope + intercept
|
|
23
|
+
# B1 0.996 0.002
|
|
24
|
+
# B2 1.001 -0.002
|
|
25
|
+
# B3 0.999 0.001
|
|
26
|
+
# B4 1.001 -0.003
|
|
27
|
+
# B5 0.998 0.004
|
|
28
|
+
# B6 0.997 0.005
|
|
29
|
+
# B7 1.000 0.000
|
|
30
|
+
# B8 0.999 0.001
|
|
31
|
+
# B8A 0.998 0.004
|
|
32
|
+
# B9 0.996 0.006
|
|
33
|
+
# B10 1.001 -0.001 B10 is not present in Sentinel-2 L2A products ommited in params below
|
|
34
|
+
# B11 0.997 0.002
|
|
35
|
+
# B12 0.998 0.003
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class L2AS2ABandpassAdjustmentParams(Enum):
|
|
39
|
+
B01 = BandpassAdjustment(0.9959, -0.0002)
|
|
40
|
+
B02 = BandpassAdjustment(0.9778, -0.004)
|
|
41
|
+
B03 = BandpassAdjustment(1.0053, -0.0009)
|
|
42
|
+
B04 = BandpassAdjustment(0.9765, 0.0009)
|
|
43
|
+
B05 = BandpassAdjustment(1.0, 0.0)
|
|
44
|
+
B06 = BandpassAdjustment(1.0, 0.0)
|
|
45
|
+
B07 = BandpassAdjustment(1.0, 0.0)
|
|
46
|
+
B08 = BandpassAdjustment(0.9983, -0.0001)
|
|
47
|
+
B8A = BandpassAdjustment(0.9983, -0.0001)
|
|
48
|
+
B09 = BandpassAdjustment(1.0, 0.0)
|
|
49
|
+
B11 = BandpassAdjustment(0.9987, -0.0011)
|
|
50
|
+
B12 = BandpassAdjustment(1.003, -0.0012)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class L2AS2BBandpassAdjustmentParams(Enum):
|
|
54
|
+
B01 = BandpassAdjustment(0.9959, -0.0002)
|
|
55
|
+
B02 = BandpassAdjustment(0.9778, -0.004)
|
|
56
|
+
B03 = BandpassAdjustment(1.0075, -0.0008)
|
|
57
|
+
B04 = BandpassAdjustment(0.9761, 0.001)
|
|
58
|
+
B05 = BandpassAdjustment(0.998, 0.004)
|
|
59
|
+
B06 = BandpassAdjustment(0.997, 0.005)
|
|
60
|
+
B07 = BandpassAdjustment(1.000, 0.000)
|
|
61
|
+
B08 = BandpassAdjustment(0.9966, 0.000)
|
|
62
|
+
B8A = BandpassAdjustment(0.9966, 0.000)
|
|
63
|
+
B09 = BandpassAdjustment(0.996, 0.006)
|
|
64
|
+
B11 = BandpassAdjustment(1.000, -0.0003)
|
|
65
|
+
B12 = BandpassAdjustment(0.9867, 0.0004)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def item_to_params(
|
|
69
|
+
sentinel2_item: Item,
|
|
70
|
+
l2a_band: L2ABand,
|
|
71
|
+
) -> BandpassAdjustment:
|
|
72
|
+
if sentinel2_item.properties["platform"].lower() == "sentinel-2a":
|
|
73
|
+
return L2AS2ABandpassAdjustmentParams[l2a_band.name].value
|
|
74
|
+
elif sentinel2_item.properties["platform"].lower() == "sentinel-2b":
|
|
75
|
+
return L2AS2BBandpassAdjustmentParams[l2a_band.name].value
|
|
76
|
+
else:
|
|
77
|
+
raise TypeError(
|
|
78
|
+
f"cannot determine Sentinel-2 platform from pystac.Item: {sentinel2_item}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def apply_bandpass_adjustment(
|
|
83
|
+
band_arr: ma.MaskedArray,
|
|
84
|
+
item: Item,
|
|
85
|
+
l2a_band: L2ABand,
|
|
86
|
+
computing_dtype: DTypeLike = np.float32,
|
|
87
|
+
out_dtype: DTypeLike = np.uint16,
|
|
88
|
+
) -> ma.MaskedArray:
|
|
89
|
+
params = item_to_params(item, l2a_band)
|
|
90
|
+
return ma.MaskedArray(
|
|
91
|
+
data=(
|
|
92
|
+
np.clip(
|
|
93
|
+
band_arr.astype(computing_dtype, copy=False) / 10000 * params.slope
|
|
94
|
+
+ params.intercept,
|
|
95
|
+
0,
|
|
96
|
+
1,
|
|
97
|
+
)
|
|
98
|
+
* 10000
|
|
99
|
+
)
|
|
100
|
+
.astype(out_dtype, copy=False)
|
|
101
|
+
.data,
|
|
102
|
+
mask=band_arr.mask,
|
|
103
|
+
fill_value=band_arr.fill_value,
|
|
104
|
+
)
|
|
@@ -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 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 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 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}")
|